mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-16 10:54:51 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b7fa7c653 |
@@ -3,10 +3,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Upload, MessageSquare, Monitor } from 'lucide-react';
|
import { Upload, MessageSquare, Monitor, TestTube } from 'lucide-react';
|
||||||
import Hero from '@/components/Hero';
|
import Hero from '@/components/Hero';
|
||||||
import { WebRTCFileTransfer } from '@/components/WebRTCFileTransfer';
|
import { WebRTCFileTransfer } from '@/components/WebRTCFileTransfer';
|
||||||
import {WebRTCTextImageTransfer} from '@/components/WebRTCTextImageTransfer';
|
import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer';
|
||||||
|
import DesktopShare from '@/components/DesktopShare';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -73,12 +74,11 @@ export default function HomePage() {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="desktop"
|
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 relative"
|
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" />
|
<Monitor className="w-4 h-4" />
|
||||||
<span className="hidden sm:inline">共享桌面</span>
|
<span className="hidden sm:inline">共享桌面</span>
|
||||||
<span className="sm:hidden">桌面</span>
|
<span className="sm:hidden">桌面</span>
|
||||||
<span className="text-xs bg-orange-100 text-orange-600 px-1.5 py-0.5 rounded ml-1 absolute -top-1 -right-1">开发中</span>
|
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,23 +94,7 @@ export default function HomePage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="desktop" className="mt-0 animate-fade-in-up">
|
<TabsContent value="desktop" className="mt-0 animate-fade-in-up">
|
||||||
<div className="max-w-md mx-auto p-8 bg-white/90 backdrop-blur-sm rounded-2xl shadow-lg border border-slate-200">
|
<DesktopShare />
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-purple-100 to-purple-200 rounded-full flex items-center justify-center">
|
|
||||||
<Monitor className="w-8 h-8 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-semibold text-slate-800 mb-2">桌面共享</h3>
|
|
||||||
<p className="text-slate-600 mb-4">此功能正在开发中...</p>
|
|
||||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-purple-700">
|
|
||||||
🚧 敬请期待!我们正在为您开发实时桌面共享功能
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-500 mt-4">
|
|
||||||
目前请使用文件传输功能
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "../styles/animations.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
|
|||||||
@@ -4,33 +4,41 @@ import React, { useState, useCallback, useEffect } from 'react';
|
|||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Share, Monitor, Copy, Play, Square } from 'lucide-react';
|
import { Share, Monitor, Copy, Play, Square, Repeat, Users, Wifi, WifiOff } from 'lucide-react';
|
||||||
import { useToast } from '@/components/ui/toast-simple';
|
import { useToast } from '@/components/ui/toast-simple';
|
||||||
|
import { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness';
|
||||||
|
import DesktopViewer from '@/components/DesktopViewer';
|
||||||
|
import QRCodeDisplay from '@/components/QRCodeDisplay';
|
||||||
|
|
||||||
interface DesktopShareProps {
|
interface DesktopShareProps {
|
||||||
onStartSharing?: () => Promise<string>; // 返回连接码
|
// 保留向后兼容性的props
|
||||||
|
onStartSharing?: () => Promise<string>;
|
||||||
onStopSharing?: () => Promise<void>;
|
onStopSharing?: () => Promise<void>;
|
||||||
onJoinSharing?: (code: string) => Promise<void>;
|
onJoinSharing?: (code: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DesktopShare({ onStartSharing, onStopSharing, onJoinSharing }: DesktopShareProps) {
|
export default function DesktopShare({
|
||||||
|
onStartSharing,
|
||||||
|
onStopSharing,
|
||||||
|
onJoinSharing
|
||||||
|
}: DesktopShareProps) {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [mode, setMode] = useState<'share' | 'view'>('share');
|
const [mode, setMode] = useState<'share' | 'view'>('share');
|
||||||
const [connectionCode, setConnectionCode] = useState('');
|
|
||||||
const [inputCode, setInputCode] = useState('');
|
const [inputCode, setInputCode] = useState('');
|
||||||
const [isSharing, setIsSharing] = useState(false);
|
|
||||||
const [isViewing, setIsViewing] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
// 使用桌面共享业务逻辑
|
||||||
|
const desktopShare = useDesktopShareBusiness();
|
||||||
|
|
||||||
// 从URL参数中获取初始模式
|
// 从URL参数中获取初始模式
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlMode = searchParams.get('mode');
|
const urlMode = searchParams.get('mode');
|
||||||
const type = searchParams.get('type');
|
const type = searchParams.get('type');
|
||||||
|
|
||||||
if (type === 'desktop' && urlMode) {
|
if (type === 'desktop' && urlMode) {
|
||||||
// 将send映射为share,receive映射为view
|
|
||||||
if (urlMode === 'send') {
|
if (urlMode === 'send') {
|
||||||
setMode('share');
|
setMode('share');
|
||||||
} else if (urlMode === 'receive') {
|
} else if (urlMode === 'receive') {
|
||||||
@@ -42,75 +50,151 @@ export default function DesktopShare({ onStartSharing, onStopSharing, onJoinShar
|
|||||||
// 更新URL参数
|
// 更新URL参数
|
||||||
const updateMode = useCallback((newMode: 'share' | 'view') => {
|
const updateMode = useCallback((newMode: 'share' | 'view') => {
|
||||||
setMode(newMode);
|
setMode(newMode);
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const currentUrl = new URL(window.location.href);
|
||||||
params.set('type', 'desktop');
|
currentUrl.searchParams.set('type', 'desktop');
|
||||||
// 将share映射为send,view映射为receive以保持一致性
|
currentUrl.searchParams.set('mode', newMode === 'share' ? 'send' : 'receive');
|
||||||
params.set('mode', newMode === 'share' ? 'send' : 'receive');
|
router.replace(currentUrl.pathname + currentUrl.search);
|
||||||
router.push(`?${params.toString()}`, { scroll: false });
|
}, [router]);
|
||||||
}, [searchParams, router]);
|
|
||||||
|
|
||||||
const handleStartSharing = useCallback(async () => {
|
// 复制房间代码
|
||||||
if (!onStartSharing) return;
|
const copyCode = useCallback(async (code: string) => {
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const code = await onStartSharing();
|
await navigator.clipboard.writeText(code);
|
||||||
setConnectionCode(code);
|
showToast('房间代码已复制到剪贴板', 'success');
|
||||||
setIsSharing(true);
|
|
||||||
showToast('桌面共享已开始!', 'success');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('开始共享失败:', error);
|
console.error('复制失败:', error);
|
||||||
showToast('开始共享失败,请重试', 'error');
|
showToast('复制失败,请手动复制', 'error');
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [onStartSharing, showToast]);
|
|
||||||
|
|
||||||
const handleStopSharing = useCallback(async () => {
|
|
||||||
if (!onStopSharing) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
await onStopSharing();
|
|
||||||
setIsSharing(false);
|
|
||||||
setConnectionCode('');
|
|
||||||
showToast('桌面共享已停止', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('停止共享失败:', error);
|
|
||||||
showToast('停止共享失败', 'error');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [onStopSharing, showToast]);
|
|
||||||
|
|
||||||
const handleJoinSharing = useCallback(async () => {
|
|
||||||
if (!inputCode.trim() || !onJoinSharing) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
await onJoinSharing(inputCode);
|
|
||||||
setIsViewing(true);
|
|
||||||
showToast('已连接到桌面共享!', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('连接失败:', error);
|
|
||||||
showToast('连接失败,请检查连接码', 'error');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [inputCode, onJoinSharing, showToast]);
|
|
||||||
|
|
||||||
const copyToClipboard = useCallback(async (text: string) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
showToast('已复制到剪贴板!', 'success');
|
|
||||||
} catch (err) {
|
|
||||||
showToast('复制失败', 'error');
|
|
||||||
}
|
}
|
||||||
}, [showToast]);
|
}, [showToast]);
|
||||||
|
|
||||||
|
// 创建房间
|
||||||
|
const handleCreateRoom = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
console.log('[DesktopShare] 用户点击创建房间');
|
||||||
|
|
||||||
|
const roomCode = await desktopShare.createRoom();
|
||||||
|
console.log('[DesktopShare] 房间创建成功:', roomCode);
|
||||||
|
|
||||||
|
showToast(`房间创建成功!代码: ${roomCode}`, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopShare] 创建房间失败:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '创建房间失败';
|
||||||
|
showToast(errorMessage, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [desktopShare, showToast]);
|
||||||
|
|
||||||
|
// 开始桌面共享
|
||||||
|
const handleStartSharing = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
console.log('[DesktopShare] 用户点击开始桌面共享');
|
||||||
|
|
||||||
|
await desktopShare.startSharing();
|
||||||
|
console.log('[DesktopShare] 桌面共享开始成功');
|
||||||
|
|
||||||
|
showToast('桌面共享已开始', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopShare] 开始桌面共享失败:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '开始桌面共享失败';
|
||||||
|
showToast(errorMessage, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [desktopShare, showToast]);
|
||||||
|
|
||||||
|
// 切换桌面
|
||||||
|
const handleSwitchDesktop = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
console.log('[DesktopShare] 用户点击切换桌面');
|
||||||
|
|
||||||
|
await desktopShare.switchDesktop();
|
||||||
|
console.log('[DesktopShare] 桌面切换成功');
|
||||||
|
|
||||||
|
showToast('桌面切换成功', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopShare] 切换桌面失败:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '切换桌面失败';
|
||||||
|
showToast(errorMessage, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [desktopShare, showToast]);
|
||||||
|
|
||||||
|
// 停止桌面共享
|
||||||
|
const handleStopSharing = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
console.log('[DesktopShare] 用户点击停止桌面共享');
|
||||||
|
|
||||||
|
await desktopShare.stopSharing();
|
||||||
|
console.log('[DesktopShare] 桌面共享停止成功');
|
||||||
|
|
||||||
|
showToast('桌面共享已停止', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopShare] 停止桌面共享失败:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '停止桌面共享失败';
|
||||||
|
showToast(errorMessage, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [desktopShare, showToast]);
|
||||||
|
|
||||||
|
// 加入观看
|
||||||
|
const handleJoinViewing = useCallback(async () => {
|
||||||
|
if (!inputCode.trim()) {
|
||||||
|
showToast('请输入房间代码', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
console.log('[DesktopShare] 用户加入观看房间:', inputCode);
|
||||||
|
|
||||||
|
await desktopShare.joinSharing(inputCode.trim().toUpperCase());
|
||||||
|
console.log('[DesktopShare] 加入观看成功');
|
||||||
|
|
||||||
|
showToast('已加入桌面共享', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopShare] 加入观看失败:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '加入观看失败';
|
||||||
|
showToast(errorMessage, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [desktopShare, inputCode, showToast]);
|
||||||
|
|
||||||
|
// 停止观看
|
||||||
|
const handleStopViewing = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await desktopShare.stopViewing();
|
||||||
|
showToast('已退出桌面共享', 'success');
|
||||||
|
setInputCode('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopShare] 停止观看失败:', error);
|
||||||
|
showToast('退出失败', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [desktopShare, showToast]);
|
||||||
|
|
||||||
|
// 连接状态指示器
|
||||||
|
const getConnectionStatus = () => {
|
||||||
|
if (desktopShare.isConnecting) return { icon: Wifi, text: '连接中...', color: 'text-yellow-600' };
|
||||||
|
if (desktopShare.isPeerConnected) return { icon: Wifi, text: 'P2P已连接', color: 'text-green-600' };
|
||||||
|
if (desktopShare.isWebSocketConnected) return { icon: Users, text: '等待对方加入', color: 'text-blue-600' };
|
||||||
|
return { icon: WifiOff, text: '未连接', color: 'text-gray-600' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectionStatus = getConnectionStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 sm:space-y-6">
|
<div className="space-y-4 sm:space-y-6">
|
||||||
{/* 模式切换 */}
|
{/* 模式选择器 */}
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-1 shadow-lg">
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-1 shadow-lg">
|
||||||
<Button
|
<Button
|
||||||
@@ -133,228 +217,424 @@ export default function DesktopShare({ onStartSharing, onStopSharing, onJoinShar
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mode === 'share' ? (
|
{mode === 'share' ? (
|
||||||
<div className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg p-4 sm:p-6 animate-fade-in-up">
|
/* 共享模式 */
|
||||||
{/* 功能标题和状态 */}
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 sm:p-6 shadow-lg border border-white/20 animate-fade-in-up">
|
||||||
<div className="flex items-center mb-6">
|
{!desktopShare.connectionCode ? (
|
||||||
<div className="flex items-center space-x-3 flex-1">
|
// 创建房间前的界面
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center">
|
<div className="space-y-6">
|
||||||
<Share className="w-5 h-5 text-white" />
|
{/* 功能标题和状态 */}
|
||||||
</div>
|
<div className="flex items-center mb-6">
|
||||||
<div>
|
<div className="flex items-center space-x-3 flex-1">
|
||||||
<h2 className="text-lg font-semibold text-slate-800">共享桌面</h2>
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-500 rounded-xl flex items-center justify-center">
|
||||||
<p className="text-sm text-slate-600">
|
<Monitor className="w-5 h-5 text-white" />
|
||||||
{isSharing ? '桌面共享进行中' : '开始共享您的桌面屏幕'}
|
</div>
|
||||||
</p>
|
<div>
|
||||||
</div>
|
<h2 className="text-lg font-semibold text-slate-800">共享桌面</h2>
|
||||||
</div>
|
<p className="text-sm text-slate-600">分享您的屏幕给其他人</p>
|
||||||
|
</div>
|
||||||
{/* 竖线分割 */}
|
|
||||||
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
|
||||||
|
|
||||||
{/* 状态显示 */}
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
|
||||||
<div className="flex items-center justify-end space-x-3 text-sm">
|
|
||||||
{/* WebSocket状态 */}
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
|
||||||
<span className="text-slate-600">WS</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分隔符 */}
|
{/* 竖线分割 */}
|
||||||
<div className="text-slate-300">|</div>
|
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
||||||
|
|
||||||
{/* WebRTC状态 */}
|
{/* 状态显示 */}
|
||||||
<div className="flex items-center space-x-1">
|
<div className="text-right">
|
||||||
{isSharing ? (
|
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
||||||
|
<div className="flex items-center justify-end space-x-3 text-sm">
|
||||||
|
{/* WebSocket状态 */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${desktopShare.isWebSocketConnected ? 'bg-blue-500 animate-pulse' : 'bg-slate-400'}`}></div>
|
||||||
|
<span className={desktopShare.isWebSocketConnected ? 'text-blue-600' : 'text-slate-600'}>WS</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分隔符 */}
|
||||||
|
<div className="text-slate-300">|</div>
|
||||||
|
|
||||||
|
{/* WebRTC状态 */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${desktopShare.isPeerConnected ? 'bg-emerald-500 animate-pulse' : 'bg-slate-400'}`}></div>
|
||||||
|
<span className={desktopShare.isPeerConnected ? 'text-emerald-600' : 'text-slate-600'}>RTC</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-purple-100 to-indigo-100 rounded-full flex items-center justify-center">
|
||||||
|
<Monitor className="w-10 h-10 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-slate-800 mb-4">创建桌面共享房间</h3>
|
||||||
|
<p className="text-slate-600 mb-8">创建房间后将生成分享码,等待接收方加入后即可开始桌面共享</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateRoom}
|
||||||
|
disabled={isLoading || desktopShare.isConnecting}
|
||||||
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||||
<span className="text-emerald-600">RTC</span>
|
创建中...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
<Share className="w-5 h-5 mr-2" />
|
||||||
<span className="text-slate-600">RTC</span>
|
创建桌面共享房间
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 房间已创建,显示取件码和等待界面
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 功能标题和状态 */}
|
||||||
|
<div className="flex items-center mb-6">
|
||||||
|
<div className="flex items-center space-x-3 flex-1">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-500 rounded-xl flex items-center justify-center">
|
||||||
|
<Monitor className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800">共享桌面</h2>
|
||||||
|
<p className="text-sm text-slate-600">
|
||||||
|
{desktopShare.isPeerConnected ? '✅ 接收方已连接,现在可以开始共享桌面' :
|
||||||
|
desktopShare.isWebSocketConnected ? '⏳ 房间已创建,等待接收方加入建立P2P连接' :
|
||||||
|
'⚠️ 等待连接'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 竖线分割 */}
|
||||||
|
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
||||||
|
|
||||||
|
{/* 状态显示 */}
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
||||||
|
<div className="flex items-center justify-end space-x-3 text-sm">
|
||||||
|
{/* WebSocket状态 */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${desktopShare.isWebSocketConnected ? 'bg-blue-500 animate-pulse' : 'bg-red-500'}`}></div>
|
||||||
|
<span className={desktopShare.isWebSocketConnected ? 'text-blue-600' : 'text-red-600'}>WS</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分隔符 */}
|
||||||
|
<div className="text-slate-300">|</div>
|
||||||
|
|
||||||
|
{/* WebRTC状态 */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${desktopShare.isPeerConnected ? 'bg-emerald-500 animate-pulse' : 'bg-orange-400'}`}></div>
|
||||||
|
<span className={desktopShare.isPeerConnected ? 'text-emerald-600' : 'text-orange-600'}>RTC</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isSharing && connectionCode && (
|
|
||||||
<div className="mt-1 text-xs text-purple-600">
|
{/* 桌面共享控制区域 */}
|
||||||
{connectionCode}
|
{desktopShare.canStartSharing && (
|
||||||
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 border border-slate-200 mb-6">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="text-lg font-medium text-slate-800 flex items-center">
|
||||||
|
<Monitor className="w-5 h-5 mr-2" />
|
||||||
|
桌面共享控制
|
||||||
|
</h4>
|
||||||
|
{desktopShare.isSharing && (
|
||||||
|
<div className="flex items-center space-x-1 bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md">
|
||||||
|
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||||
|
<span className="font-medium">共享中</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{!desktopShare.isSharing ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleStartSharing}
|
||||||
|
disabled={isLoading || !desktopShare.isPeerConnected}
|
||||||
|
className={`w-full px-8 py-3 text-lg font-medium rounded-xl shadow-lg ${
|
||||||
|
desktopShare.isPeerConnected
|
||||||
|
? 'bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 text-white'
|
||||||
|
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Play className="w-5 h-5 mr-2" />
|
||||||
|
{isLoading ? '启动中...' : '选择并开始共享桌面'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!desktopShare.isPeerConnected && (
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-gray-500 mb-2">
|
||||||
|
等待接收方加入房间建立P2P连接...
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-purple-500"></div>
|
||||||
|
<span className="text-sm text-purple-600">正在等待连接</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-center space-x-2 text-green-600 mb-4">
|
||||||
|
<Play className="w-5 h-5" />
|
||||||
|
<span className="font-semibold">桌面共享进行中</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center space-x-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleSwitchDesktop}
|
||||||
|
disabled={isLoading}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Repeat className="w-4 h-4 mr-2" />
|
||||||
|
{isLoading ? '切换中...' : '切换桌面'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleStopSharing}
|
||||||
|
disabled={isLoading}
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Square className="w-4 h-4 mr-2" />
|
||||||
|
{isLoading ? '停止中...' : '停止共享'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* 取件码显示 - 和文件传输一致的风格 */}
|
||||||
{!isSharing ? (
|
<div className="border-t border-slate-200 pt-6">
|
||||||
<Button
|
{/* 左上角状态提示 */}
|
||||||
onClick={handleStartSharing}
|
<div className="flex items-center mb-6">
|
||||||
disabled={isLoading}
|
<div className="flex items-center space-x-3">
|
||||||
className="w-full h-12 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
|
||||||
>
|
<Monitor className="w-5 h-5 text-white" />
|
||||||
{isLoading ? (
|
</div>
|
||||||
<>
|
<div>
|
||||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
<h3 className="text-lg font-semibold text-slate-800">房间码生成成功!</h3>
|
||||||
启动中...
|
<p className="text-sm text-slate-600">分享以下信息给观看方</p>
|
||||||
</>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<>
|
</div>
|
||||||
<Play className="w-5 h-5 mr-2" />
|
|
||||||
开始共享桌面
|
{/* 中间区域:取件码 + 分隔线 + 二维码 */}
|
||||||
</>
|
<div className="flex flex-col lg:flex-row lg:items-start gap-6 lg:gap-8 mb-8">
|
||||||
)}
|
{/* 左侧:取件码 */}
|
||||||
</Button>
|
<div className="flex-1">
|
||||||
) : (
|
<label className="block text-sm font-medium text-slate-700 mb-3">房间代码</label>
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col items-center rounded-xl border border-slate-200 p-6 h-40 justify-center bg-slate-50">
|
||||||
<div className="p-4 bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl border border-purple-200">
|
<div className="text-2xl font-bold font-mono bg-gradient-to-r from-purple-600 to-indigo-600 bg-clip-text text-transparent tracking-wider">
|
||||||
<div className="text-center">
|
{desktopShare.connectionCode}
|
||||||
<p className="text-sm text-purple-700 mb-2">连接码</p>
|
</div>
|
||||||
<div className="text-2xl font-bold font-mono text-purple-600 mb-3">{connectionCode}</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => copyToClipboard(connectionCode)}
|
onClick={() => copyCode(desktopShare.connectionCode)}
|
||||||
size="sm"
|
className="w-full px-4 py-2.5 bg-purple-500 hover:bg-purple-600 text-white rounded-lg font-medium shadow transition-all duration-200 mt-3"
|
||||||
className="bg-purple-500 hover:bg-purple-600 text-white"
|
|
||||||
>
|
>
|
||||||
<Copy className="w-4 h-4 mr-2" />
|
复制房间代码
|
||||||
复制连接码
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分隔线 - 大屏幕显示竖线,移动端隐藏 */}
|
||||||
|
<div className="hidden lg:block w-px bg-slate-200 h-64 mt-6"></div>
|
||||||
|
|
||||||
|
{/* 右侧:二维码 */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-3">扫码观看</label>
|
||||||
|
<div className="flex flex-col items-center rounded-xl border border-slate-200 p-6 h-40 justify-center bg-slate-50">
|
||||||
|
<QRCodeDisplay
|
||||||
|
value={`${typeof window !== 'undefined' ? window.location.origin : ''}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`}
|
||||||
|
size={120}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full px-4 py-2.5 bg-blue-500 text-white rounded-lg font-medium shadow transition-all duration-200 mt-3 text-center">
|
||||||
|
使用手机扫码快速观看
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部:观看链接 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-1 code-display rounded-lg p-3 bg-slate-50 border border-slate-200">
|
||||||
|
<div className="text-sm text-slate-700 break-all font-mono leading-relaxed">
|
||||||
|
{`${typeof window !== 'undefined' ? window.location.origin : ''}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const link = `${window.location.origin}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`;
|
||||||
|
navigator.clipboard.writeText(link);
|
||||||
|
showToast('观看链接已复制', 'success');
|
||||||
|
}}
|
||||||
|
className="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-medium shadow transition-all duration-200 shrink-0"
|
||||||
|
>
|
||||||
|
复制链接
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 观看模式 */
|
||||||
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 sm:p-6 shadow-lg border border-white/20 animate-fade-in-up">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{!desktopShare.isViewing ? (
|
||||||
|
// 输入房间代码界面 - 与文本消息风格一致
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center mb-6 sm:mb-8">
|
||||||
|
<div className="flex items-center space-x-3 flex-1">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-500 rounded-xl flex items-center justify-center">
|
||||||
|
<Monitor className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800">输入房间代码</h2>
|
||||||
|
<p className="text-sm text-slate-600">请输入6位房间代码来观看桌面共享</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={(e) => { e.preventDefault(); handleJoinViewing(); }} className="space-y-4 sm:space-y-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
value={inputCode}
|
||||||
|
onChange={(e) => setInputCode(e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase())}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-xs sm:text-sm text-slate-500">
|
||||||
|
{inputCode.length}/6 位
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={inputCode.length !== 6 || isLoading}
|
||||||
|
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 ? (
|
||||||
|
<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>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Monitor className="w-5 h-5" />
|
||||||
|
<span>加入观看</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 已连接,显示桌面观看界面
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center mb-6">
|
||||||
|
<div className="flex items-center space-x-3 flex-1">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
|
||||||
|
<Monitor className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-800">桌面观看</h3>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
<span className="text-emerald-600">✅ 已连接,正在观看桌面共享</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 连接成功状态 */}
|
||||||
|
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-6">
|
||||||
|
<h4 className="font-semibold text-emerald-800 mb-1">已连接到桌面共享房间</h4>
|
||||||
|
<p className="text-emerald-700">房间代码: {inputCode}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 观看中的控制面板 */}
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<div className="bg-white rounded-lg p-3 shadow-lg border flex items-center space-x-4">
|
||||||
|
<div className="flex items-center space-x-2 text-green-600">
|
||||||
|
<Monitor className="w-4 h-4" />
|
||||||
|
<span className="font-semibold">观看中</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleStopViewing}
|
||||||
|
disabled={isLoading}
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Square className="w-4 h-4 mr-2" />
|
||||||
|
{isLoading ? '退出中...' : '退出观看'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
{/* 桌面显示区域 */}
|
||||||
onClick={handleStopSharing}
|
{desktopShare.remoteStream ? (
|
||||||
disabled={isLoading}
|
<DesktopViewer
|
||||||
className="w-full h-12 bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
stream={desktopShare.remoteStream}
|
||||||
>
|
isConnected={desktopShare.isViewing}
|
||||||
{isLoading ? (
|
connectionCode={inputCode}
|
||||||
<>
|
onDisconnect={handleStopViewing}
|
||||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
/>
|
||||||
停止中...
|
) : (
|
||||||
</>
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-8 border border-slate-200">
|
||||||
) : (
|
<div className="text-center">
|
||||||
<>
|
<Monitor className="w-16 h-16 mx-auto text-slate-400 mb-4" />
|
||||||
<Square className="w-5 h-5 mr-2" />
|
<p className="text-slate-600 mb-2">等待接收桌面画面...</p>
|
||||||
停止共享
|
<p className="text-sm text-slate-500">发送方开始共享后,桌面画面将在这里显示</p>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg p-4 sm:p-6 animate-fade-in-up">
|
|
||||||
{/* 功能标题和状态 */}
|
|
||||||
<div className="flex items-center mb-6">
|
|
||||||
<div className="flex items-center space-x-3 flex-1">
|
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-xl flex items-center justify-center">
|
|
||||||
<Monitor className="w-5 h-5 text-white" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-slate-800">观看桌面</h2>
|
|
||||||
<p className="text-sm text-slate-600">
|
|
||||||
{isViewing ? '正在观看桌面共享' : '输入连接码观看他人的桌面'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 竖线分割 */}
|
<div className="flex items-center justify-center space-x-2 mt-4">
|
||||||
<div className="w-px h-12 bg-slate-200 mx-4"></div>
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-purple-500"></div>
|
||||||
|
<span className="text-sm text-purple-600">等待桌面流...</span>
|
||||||
{/* 状态显示 */}
|
</div>
|
||||||
<div className="text-right">
|
</div>
|
||||||
<div className="text-sm text-slate-500 mb-1">连接状态</div>
|
|
||||||
<div className="flex items-center justify-end space-x-3 text-sm">
|
|
||||||
{/* WebSocket状态 */}
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
|
||||||
<span className="text-slate-600">WS</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 分隔符 */}
|
|
||||||
<div className="text-slate-300">|</div>
|
|
||||||
|
|
||||||
{/* WebRTC状态 */}
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{isViewing ? (
|
|
||||||
<>
|
|
||||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
|
||||||
<span className="text-emerald-600">RTC</span>
|
|
||||||
</>
|
|
||||||
) : isLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
|
|
||||||
<span className="text-orange-600">RTC</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
|
|
||||||
<span className="text-slate-600">RTC</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isViewing && (
|
|
||||||
<div className="mt-1 text-xs text-indigo-600">
|
|
||||||
观看中
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{!isViewing ? (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
value={inputCode}
|
|
||||||
onChange={(e) => setInputCode(e.target.value.toUpperCase().slice(0, 6))}
|
|
||||||
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-indigo-500 focus:ring-indigo-500 bg-white/80 backdrop-blur-sm"
|
|
||||||
maxLength={6}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleJoinSharing}
|
|
||||||
disabled={inputCode.length !== 6 || isLoading}
|
|
||||||
className="w-full h-12 bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
|
||||||
连接中...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Monitor className="w-5 h-5 mr-2" />
|
|
||||||
连接桌面
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="aspect-video bg-slate-900 rounded-xl flex items-center justify-center text-white">
|
|
||||||
<div className="text-center">
|
|
||||||
<Monitor className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
|
||||||
<p className="text-sm opacity-75">桌面共享画面</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => setIsViewing(false)}
|
|
||||||
className="w-full h-12 bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
|
||||||
>
|
|
||||||
<Square className="w-5 h-5 mr-2" />
|
|
||||||
断开连接
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 错误显示 */}
|
||||||
|
{desktopShare.error && (
|
||||||
|
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-red-600 text-sm">{desktopShare.error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 调试信息 */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDebug(!showDebug)}
|
||||||
|
className="text-xs text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
{showDebug ? '隐藏' : '显示'}调试信息
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showDebug && (
|
||||||
|
<div className="mt-2 p-3 bg-gray-50 rounded text-xs text-gray-600 space-y-1">
|
||||||
|
<div>WebSocket连接: {desktopShare.isWebSocketConnected ? '✅' : '❌'}</div>
|
||||||
|
<div>P2P连接: {desktopShare.isPeerConnected ? '✅' : '❌'}</div>
|
||||||
|
<div>房间代码: {desktopShare.connectionCode || '未创建'}</div>
|
||||||
|
<div>共享状态: {desktopShare.isSharing ? '进行中' : '未共享'}</div>
|
||||||
|
<div>观看状态: {desktopShare.isViewing ? '观看中' : '未观看'}</div>
|
||||||
|
<div>等待对方: {desktopShare.isWaitingForPeer ? '是' : '否'}</div>
|
||||||
|
<div>远程流: {desktopShare.remoteStream ? '已接收' : '无'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
344
chuan-next/src/components/DesktopViewer.tsx
Normal file
344
chuan-next/src/components/DesktopViewer.tsx
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
|
import { Monitor, Maximize, Minimize, Volume2, VolumeX, Settings, X } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
interface DesktopViewerProps {
|
||||||
|
stream: MediaStream | null;
|
||||||
|
isConnected: boolean;
|
||||||
|
connectionCode?: string;
|
||||||
|
onDisconnect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DesktopViewer({
|
||||||
|
stream,
|
||||||
|
isConnected,
|
||||||
|
connectionCode,
|
||||||
|
onDisconnect
|
||||||
|
}: DesktopViewerProps) {
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
|
const [showControls, setShowControls] = useState(true);
|
||||||
|
const [videoStats, setVideoStats] = useState<{
|
||||||
|
resolution: string;
|
||||||
|
fps: number;
|
||||||
|
}>({ resolution: '0x0', fps: 0 });
|
||||||
|
|
||||||
|
const hideControlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// 设置视频流
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoRef.current && stream) {
|
||||||
|
console.log('[DesktopViewer] 🎬 设置视频流,轨道数量:', stream.getTracks().length);
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
console.log('[DesktopViewer] 轨道详情:', track.kind, track.id, track.enabled, track.readyState);
|
||||||
|
});
|
||||||
|
|
||||||
|
videoRef.current.srcObject = stream;
|
||||||
|
console.log('[DesktopViewer] ✅ 视频元素已设置流');
|
||||||
|
} else if (videoRef.current && !stream) {
|
||||||
|
console.log('[DesktopViewer] ❌ 清除视频流');
|
||||||
|
videoRef.current.srcObject = null;
|
||||||
|
}
|
||||||
|
}, [stream]);
|
||||||
|
|
||||||
|
// 监控视频统计信息
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoRef.current) return;
|
||||||
|
|
||||||
|
const video = videoRef.current;
|
||||||
|
const updateStats = () => {
|
||||||
|
if (video.videoWidth && video.videoHeight) {
|
||||||
|
setVideoStats({
|
||||||
|
resolution: `${video.videoWidth}x${video.videoHeight}`,
|
||||||
|
fps: 0, // 实际FPS需要更复杂的计算
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('loadedmetadata', updateStats);
|
||||||
|
video.addEventListener('resize', updateStats);
|
||||||
|
|
||||||
|
const interval = setInterval(updateStats, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('loadedmetadata', updateStats);
|
||||||
|
video.removeEventListener('resize', updateStats);
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 全屏相关处理
|
||||||
|
useEffect(() => {
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
const isCurrentlyFullscreen = !!document.fullscreenElement;
|
||||||
|
setIsFullscreen(isCurrentlyFullscreen);
|
||||||
|
|
||||||
|
if (isCurrentlyFullscreen) {
|
||||||
|
// 全屏时自动隐藏控制栏,鼠标移动时显示
|
||||||
|
setShowControls(false);
|
||||||
|
} else {
|
||||||
|
setShowControls(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 鼠标移动处理(全屏时)
|
||||||
|
const handleMouseMove = useCallback(() => {
|
||||||
|
if (isFullscreen) {
|
||||||
|
setShowControls(true);
|
||||||
|
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (hideControlsTimeoutRef.current) {
|
||||||
|
clearTimeout(hideControlsTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3秒后自动隐藏控制栏
|
||||||
|
hideControlsTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowControls(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, [isFullscreen]);
|
||||||
|
|
||||||
|
// 键盘快捷键
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Escape':
|
||||||
|
if (isFullscreen) {
|
||||||
|
exitFullscreen();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
case 'F':
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'm':
|
||||||
|
case 'M':
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleMute();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isFullscreen]);
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
const toggleFullscreen = useCallback(async () => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isFullscreen) {
|
||||||
|
await document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
await containerRef.current.requestFullscreen();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopViewer] 全屏切换失败:', error);
|
||||||
|
}
|
||||||
|
}, [isFullscreen]);
|
||||||
|
|
||||||
|
// 退出全屏
|
||||||
|
const exitFullscreen = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
await document.exitFullscreen();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopViewer] 退出全屏失败:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 切换静音
|
||||||
|
const toggleMute = useCallback(() => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.muted = !videoRef.current.muted;
|
||||||
|
setIsMuted(videoRef.current.muted);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (hideControlsTimeoutRef.current) {
|
||||||
|
clearTimeout(hideControlsTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!stream) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-96 bg-slate-900 rounded-xl text-white">
|
||||||
|
<Monitor className="w-16 h-16 opacity-50 mb-4" />
|
||||||
|
<p className="text-lg opacity-75">
|
||||||
|
{isConnected ? '等待桌面共享流...' : '等待桌面共享连接...'}
|
||||||
|
</p>
|
||||||
|
{connectionCode && (
|
||||||
|
<p className="text-sm opacity-50 mt-2">连接码: {connectionCode}</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-4 flex items-center space-x-2 text-sm">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500 animate-pulse' : 'bg-yellow-500 animate-pulse'}`}></div>
|
||||||
|
<span>{isConnected ? '已连接,等待视频流' : '正在建立连接'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`relative bg-black rounded-xl overflow-hidden ${isFullscreen ? 'fixed inset-0 z-50' : 'w-full'}`}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseEnter={() => isFullscreen && setShowControls(true)}
|
||||||
|
>
|
||||||
|
{/* 主视频显示 */}
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
muted={isMuted}
|
||||||
|
className={`w-full h-full object-contain ${isFullscreen ? 'cursor-none' : ''}`}
|
||||||
|
style={{
|
||||||
|
aspectRatio: isFullscreen ? 'unset' : '16/9',
|
||||||
|
minHeight: isFullscreen ? '100vh' : '400px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 连接状态覆盖层 */}
|
||||||
|
{!isConnected && (
|
||||||
|
<div className="absolute inset-0 bg-black/80 flex flex-col items-center justify-center text-white">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mb-4"></div>
|
||||||
|
<p className="text-lg">正在连接桌面共享...</p>
|
||||||
|
{connectionCode && (
|
||||||
|
<p className="text-sm opacity-75 mt-2">连接码: {connectionCode}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 控制栏 */}
|
||||||
|
<div
|
||||||
|
className={`absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-4 transition-all duration-300 ${
|
||||||
|
showControls || !isFullscreen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{/* 左侧信息 */}
|
||||||
|
<div className="flex items-center space-x-4 text-white text-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
|
||||||
|
<span>桌面共享中</span>
|
||||||
|
</div>
|
||||||
|
{videoStats.resolution !== '0x0' && (
|
||||||
|
<>
|
||||||
|
<div className="w-px h-4 bg-white/30"></div>
|
||||||
|
<span>{videoStats.resolution}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{connectionCode && (
|
||||||
|
<>
|
||||||
|
<div className="w-px h-4 bg-white/30"></div>
|
||||||
|
<span className="font-mono">{connectionCode}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧控制按钮 */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{/* 音频控制 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleMute}
|
||||||
|
className="text-white hover:bg-white/20"
|
||||||
|
>
|
||||||
|
{isMuted ? (
|
||||||
|
<VolumeX className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Volume2 className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 设置 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-white hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 全屏切换 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
className="text-white hover:bg-white/20"
|
||||||
|
title={isFullscreen ? "退出全屏 (Esc)" : "全屏 (Ctrl+F)"}
|
||||||
|
>
|
||||||
|
{isFullscreen ? (
|
||||||
|
<Minimize className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Maximize className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 断开连接 */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onDisconnect}
|
||||||
|
className="text-white hover:bg-red-500/30"
|
||||||
|
title="断开连接"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 快捷键提示(仅全屏时显示) */}
|
||||||
|
{isFullscreen && showControls && (
|
||||||
|
<div className="mt-2 text-xs text-white/60 text-center">
|
||||||
|
<p>快捷键: Esc 退出全屏 | Ctrl+F 切换全屏 | Ctrl+M 切换静音</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 加载状态 */}
|
||||||
|
{stream && !isConnected && (
|
||||||
|
<div className="absolute top-4 left-4 bg-black/60 text-white px-3 py-2 rounded-lg text-sm flex items-center space-x-2">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||||
|
<span>建立连接中...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 网络状态指示器 */}
|
||||||
|
<div className="absolute top-4 right-4 bg-black/60 text-white px-3 py-2 rounded-lg text-xs">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${
|
||||||
|
isConnected ? 'bg-green-500' : 'bg-yellow-500 animate-pulse'
|
||||||
|
}`}></div>
|
||||||
|
<span>{isConnected ? '已连接' : '连接中'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -287,8 +287,8 @@ export const WebRTCFileTransfer: React.FC = () => {
|
|||||||
const updatedList = [...prev, ...newFileInfos];
|
const updatedList = [...prev, ...newFileInfos];
|
||||||
console.log('更新后的文件列表:', updatedList);
|
console.log('更新后的文件列表:', updatedList);
|
||||||
|
|
||||||
// 如果已连接,立即同步文件列表
|
// 如果P2P连接已建立,立即同步文件列表
|
||||||
if (isConnected && pickupCode) {
|
if (isConnected && connection.isPeerConnected && pickupCode) {
|
||||||
console.log('立即同步文件列表到对端');
|
console.log('立即同步文件列表到对端');
|
||||||
setTimeout(() => sendFileList(updatedList), 100);
|
setTimeout(() => sendFileList(updatedList), 100);
|
||||||
}
|
}
|
||||||
@@ -573,18 +573,23 @@ export const WebRTCFileTransfer: React.FC = () => {
|
|||||||
console.log('WebRTC连接状态:', isConnected);
|
console.log('WebRTC连接状态:', isConnected);
|
||||||
console.log('连接中状态:', isConnecting);
|
console.log('连接中状态:', isConnecting);
|
||||||
|
|
||||||
// 如果WebSocket断开但不是主动断开的情况
|
// 只有在之前已经建立过连接,现在断开的情况下才显示断开提示
|
||||||
|
// 避免在初始连接时误报断开
|
||||||
if (!isWebSocketConnected && !isConnected && !isConnecting && pickupCode) {
|
if (!isWebSocketConnected && !isConnected && !isConnecting && pickupCode) {
|
||||||
showToast('与服务器的连接已断开,请重新连接', "error");
|
// 增加额外检查:只有在之前曾经连接成功过的情况下才显示断开提示
|
||||||
|
// 通过检查是否有文件列表来判断是否曾经连接过
|
||||||
|
if (fileList.length > 0 || currentTransferFile) {
|
||||||
|
showToast('与服务器的连接已断开,请重新连接', "error");
|
||||||
|
|
||||||
// 清理传输状态
|
// 清理传输状态
|
||||||
console.log('WebSocket断开,清理传输状态');
|
console.log('WebSocket断开,清理传输状态');
|
||||||
setCurrentTransferFile(null);
|
setCurrentTransferFile(null);
|
||||||
setFileList(prev => prev.map(item =>
|
setFileList(prev => prev.map(item =>
|
||||||
item.status === 'downloading'
|
item.status === 'downloading'
|
||||||
? { ...item, status: 'ready' as const, progress: 0 }
|
? { ...item, status: 'ready' as const, progress: 0 }
|
||||||
: item
|
: item
|
||||||
));
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket连接成功时的提示
|
// WebSocket连接成功时的提示
|
||||||
@@ -592,7 +597,7 @@ export const WebRTCFileTransfer: React.FC = () => {
|
|||||||
console.log('WebSocket已连接,正在建立P2P连接...');
|
console.log('WebSocket已连接,正在建立P2P连接...');
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [isWebSocketConnected, isConnected, isConnecting, pickupCode, showToast]);
|
}, [isWebSocketConnected, isConnected, isConnecting, pickupCode, showToast, fileList.length, currentTransferFile]);
|
||||||
|
|
||||||
// 监听连接状态变化,清理传输状态
|
// 监听连接状态变化,清理传输状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -646,8 +651,8 @@ export const WebRTCFileTransfer: React.FC = () => {
|
|||||||
console.log('正在建立WebRTC连接...');
|
console.log('正在建立WebRTC连接...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有在连接成功且没有错误时才发送文件列表
|
// 只有在P2P连接建立且没有错误时才发送文件列表
|
||||||
if (isConnected && !error && pickupCode && mode === 'send' && selectedFiles.length > 0) {
|
if (isConnected && connection.isPeerConnected && !error && pickupCode && mode === 'send' && selectedFiles.length > 0) {
|
||||||
// 确保有文件列表
|
// 确保有文件列表
|
||||||
if (fileList.length === 0) {
|
if (fileList.length === 0) {
|
||||||
console.log('创建文件列表并发送...');
|
console.log('创建文件列表并发送...');
|
||||||
@@ -662,7 +667,7 @@ export const WebRTCFileTransfer: React.FC = () => {
|
|||||||
setFileList(newFileInfos);
|
setFileList(newFileInfos);
|
||||||
// 延迟发送,确保数据通道已准备好
|
// 延迟发送,确保数据通道已准备好
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isConnected && !error) { // 再次检查连接状态
|
if (isConnected && connection.isPeerConnected && !error) { // 再次检查连接状态
|
||||||
sendFileList(newFileInfos);
|
sendFileList(newFileInfos);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -670,13 +675,26 @@ export const WebRTCFileTransfer: React.FC = () => {
|
|||||||
console.log('发送现有文件列表...');
|
console.log('发送现有文件列表...');
|
||||||
// 延迟发送,确保数据通道已准备好
|
// 延迟发送,确保数据通道已准备好
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isConnected && !error) { // 再次检查连接状态
|
if (isConnected && connection.isPeerConnected && !error) { // 再次检查连接状态
|
||||||
sendFileList(fileList);
|
sendFileList(fileList);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isConnected, isConnecting, isWebSocketConnected, pickupCode, mode, selectedFiles.length, error]);
|
}, [isConnected, connection.isPeerConnected, isConnecting, isWebSocketConnected, pickupCode, mode, selectedFiles.length, error]);
|
||||||
|
|
||||||
|
// 监听P2P连接建立,自动发送文件列表
|
||||||
|
useEffect(() => {
|
||||||
|
if (connection.isPeerConnected && mode === 'send' && fileList.length > 0) {
|
||||||
|
console.log('P2P连接已建立,发送文件列表...');
|
||||||
|
// 稍微延迟一下,确保数据通道完全准备好
|
||||||
|
setTimeout(() => {
|
||||||
|
if (connection.isPeerConnected && connection.getChannelState() === 'open') {
|
||||||
|
sendFileList(fileList);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}, [connection.isPeerConnected, mode, fileList.length, sendFileList]);
|
||||||
|
|
||||||
// 请求下载文件(接收方调用)
|
// 请求下载文件(接收方调用)
|
||||||
const requestFile = (fileId: string) => {
|
const requestFile = (fileId: string) => {
|
||||||
@@ -765,7 +783,8 @@ export const WebRTCFileTransfer: React.FC = () => {
|
|||||||
console.log('=== 清空文件 ===');
|
console.log('=== 清空文件 ===');
|
||||||
setSelectedFiles([]);
|
setSelectedFiles([]);
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
if (isConnected && pickupCode) {
|
// 只有在P2P连接建立且数据通道准备好时才发送清空消息
|
||||||
|
if (isConnected && connection.isPeerConnected && connection.getChannelState() === 'open' && pickupCode) {
|
||||||
sendFileList([]);
|
sendFileList([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,8 +106,7 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
|
|||||||
setIsTyping(false);
|
setIsTyping(false);
|
||||||
|
|
||||||
// 断开连接
|
// 断开连接
|
||||||
textTransfer.disconnect();
|
connection.disconnect();
|
||||||
fileTransfer.disconnect();
|
|
||||||
|
|
||||||
if (onRestart) {
|
if (onRestart) {
|
||||||
onRestart();
|
onRestart();
|
||||||
|
|||||||
@@ -38,11 +38,9 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
|
|||||||
// 连接所有传输通道
|
// 连接所有传输通道
|
||||||
const connectAll = useCallback(async (code: string, role: 'sender' | 'receiver') => {
|
const connectAll = useCallback(async (code: string, role: 'sender' | 'receiver') => {
|
||||||
console.log('=== 连接所有传输通道 ===', { code, role });
|
console.log('=== 连接所有传输通道 ===', { code, role });
|
||||||
await Promise.all([
|
// 只需要连接一次,因为使用的是共享连接
|
||||||
textTransfer.connect(code, role),
|
await connection.connect(code, role);
|
||||||
fileTransfer.connect(code, role)
|
}, [connection]);
|
||||||
]);
|
|
||||||
}, [textTransfer, fileTransfer]);
|
|
||||||
|
|
||||||
// 是否有任何连接
|
// 是否有任何连接
|
||||||
const hasAnyConnection = textTransfer.isConnected || fileTransfer.isConnected;
|
const hasAnyConnection = textTransfer.isConnected || fileTransfer.isConnected;
|
||||||
@@ -63,9 +61,8 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
|
|||||||
sentImages.forEach(img => URL.revokeObjectURL(img.url));
|
sentImages.forEach(img => URL.revokeObjectURL(img.url));
|
||||||
setSentImages([]);
|
setSentImages([]);
|
||||||
|
|
||||||
// 断开连接
|
// 断开连接(只需要断开一次)
|
||||||
textTransfer.disconnect();
|
connection.disconnect();
|
||||||
fileTransfer.disconnect();
|
|
||||||
|
|
||||||
if (onRestart) {
|
if (onRestart) {
|
||||||
onRestart();
|
onRestart();
|
||||||
@@ -141,7 +138,7 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
|
|||||||
// 如果有初始文本,发送它
|
// 如果有初始文本,发送它
|
||||||
if (currentText) {
|
if (currentText) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (textTransfer.isConnected) {
|
if (connection.isPeerConnected && textTransfer.isConnected) {
|
||||||
// 发送实时文本同步
|
// 发送实时文本同步
|
||||||
textTransfer.sendTextSync(currentText);
|
textTransfer.sendTextSync(currentText);
|
||||||
|
|
||||||
@@ -171,8 +168,8 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
|
|||||||
const newHeight = Math.min(Math.max(textarea.scrollHeight, 100), 300); // 最小100px,最大300px
|
const newHeight = Math.min(Math.max(textarea.scrollHeight, 100), 300); // 最小100px,最大300px
|
||||||
textarea.style.height = `${newHeight}px`;
|
textarea.style.height = `${newHeight}px`;
|
||||||
|
|
||||||
// 实时同步文本内容(如果已连接)
|
// 实时同步文本内容(如果P2P连接已建立)
|
||||||
if (textTransfer.isConnected) {
|
if (connection.isPeerConnected && textTransfer.isConnected) {
|
||||||
// 发送实时文本同步
|
// 发送实时文本同步
|
||||||
textTransfer.sendTextSync(value);
|
textTransfer.sendTextSync(value);
|
||||||
|
|
||||||
@@ -215,9 +212,11 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
|
|||||||
}]);
|
}]);
|
||||||
|
|
||||||
// 发送文件
|
// 发送文件
|
||||||
if (fileTransfer.isConnected) {
|
if (connection.isPeerConnected && fileTransfer.isConnected) {
|
||||||
fileTransfer.sendFile(file);
|
fileTransfer.sendFile(file);
|
||||||
showToast('图片发送中...', "success");
|
showToast('图片发送中...', "success");
|
||||||
|
} else if (!connection.isPeerConnected) {
|
||||||
|
showToast('等待对方加入P2P网络...', "error");
|
||||||
} else {
|
} else {
|
||||||
showToast('请先连接到房间', "error");
|
showToast('请先连接到房间', "error");
|
||||||
}
|
}
|
||||||
@@ -409,8 +408,16 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
|
|||||||
value={textInput}
|
value={textInput}
|
||||||
onChange={handleTextInputChange}
|
onChange={handleTextInputChange}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder="在这里编辑文字内容... 💡 支持实时同步编辑,对方可以看到你的修改 💡 可以直接粘贴图片 (Ctrl+V)"
|
disabled={!connection.isPeerConnected}
|
||||||
className="w-full h-40 px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-slate-700 placeholder-slate-400"
|
placeholder={connection.isPeerConnected
|
||||||
|
? "在这里编辑文字内容... 💡 支持实时同步编辑,对方可以看到你的修改 💡 可以直接粘贴图片 (Ctrl+V)"
|
||||||
|
: "等待对方加入P2P网络... 📡 建立连接后即可开始输入文字"
|
||||||
|
}
|
||||||
|
className={`w-full h-40 px-4 py-3 border rounded-lg resize-none text-slate-700 ${
|
||||||
|
connection.isPeerConnected
|
||||||
|
? "border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-slate-400"
|
||||||
|
: "border-slate-200 bg-slate-50 cursor-not-allowed placeholder-slate-300"
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-between mt-3">
|
<div className="flex items-center justify-between mt-3">
|
||||||
@@ -419,7 +426,10 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
|
|||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex items-center space-x-1"
|
disabled={!connection.isPeerConnected}
|
||||||
|
className={`flex items-center space-x-1 ${
|
||||||
|
!connection.isPeerConnected ? 'cursor-not-allowed opacity-50' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Image className="w-4 h-4" />
|
<Image className="w-4 h-4" />
|
||||||
<span>添加图片</span>
|
<span>添加图片</span>
|
||||||
|
|||||||
407
chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts
Normal file
407
chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
|
import { useSharedWebRTCManager } from './useSharedWebRTCManager';
|
||||||
|
|
||||||
|
interface DesktopShareState {
|
||||||
|
isSharing: boolean;
|
||||||
|
isViewing: boolean;
|
||||||
|
connectionCode: string;
|
||||||
|
remoteStream: MediaStream | null;
|
||||||
|
error: string | null;
|
||||||
|
isWaitingForPeer: boolean; // 新增:是否等待对方连接
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesktopShareBusiness() {
|
||||||
|
const webRTC = useSharedWebRTCManager();
|
||||||
|
const [state, setState] = useState<DesktopShareState>({
|
||||||
|
isSharing: false,
|
||||||
|
isViewing: false,
|
||||||
|
connectionCode: '',
|
||||||
|
remoteStream: null,
|
||||||
|
error: null,
|
||||||
|
isWaitingForPeer: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const localStreamRef = useRef<MediaStream | null>(null);
|
||||||
|
const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
const currentSenderRef = useRef<RTCRtpSender | null>(null);
|
||||||
|
|
||||||
|
const updateState = useCallback((updates: Partial<DesktopShareState>) => {
|
||||||
|
setState(prev => ({ ...prev, ...updates }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 生成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 {
|
||||||
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: {
|
||||||
|
cursor: 'always',
|
||||||
|
displaySurface: 'monitor',
|
||||||
|
} as DisplayMediaStreamOptions['video'],
|
||||||
|
audio: {
|
||||||
|
echoCancellation: false,
|
||||||
|
noiseSuppression: false,
|
||||||
|
autoGainControl: false,
|
||||||
|
} as DisplayMediaStreamOptions['audio'],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DesktopShare] 获取桌面流成功:', stream.getTracks().length, '个轨道');
|
||||||
|
return stream;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DesktopShare] 获取桌面流失败:', error);
|
||||||
|
throw new Error('无法获取桌面共享权限,请确保允许屏幕共享');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 设置视频轨道发送
|
||||||
|
const setupVideoSending = useCallback(async (stream: MediaStream) => {
|
||||||
|
console.log('[DesktopShare] 🎬 开始设置视频轨道发送...');
|
||||||
|
|
||||||
|
// 移除之前的轨道(如果存在)
|
||||||
|
if (currentSenderRef.current) {
|
||||||
|
console.log('[DesktopShare] 🗑️ 移除之前的视频轨道');
|
||||||
|
webRTC.removeTrack(currentSenderRef.current);
|
||||||
|
currentSenderRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新的视频轨道到PeerConnection
|
||||||
|
const videoTrack = stream.getVideoTracks()[0];
|
||||||
|
const audioTrack = stream.getAudioTracks()[0];
|
||||||
|
|
||||||
|
if (videoTrack) {
|
||||||
|
console.log('[DesktopShare] 📹 添加视频轨道:', videoTrack.id, videoTrack.readyState);
|
||||||
|
const videoSender = webRTC.addTrack(videoTrack, stream);
|
||||||
|
if (videoSender) {
|
||||||
|
currentSenderRef.current = videoSender;
|
||||||
|
console.log('[DesktopShare] ✅ 视频轨道添加成功');
|
||||||
|
} else {
|
||||||
|
console.warn('[DesktopShare] ⚠️ 视频轨道添加返回null');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('[DesktopShare] ❌ 未找到视频轨道');
|
||||||
|
throw new Error('未找到视频轨道');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioTrack) {
|
||||||
|
try {
|
||||||
|
console.log('[DesktopShare] 🎵 添加音频轨道:', audioTrack.id, audioTrack.readyState);
|
||||||
|
const audioSender = webRTC.addTrack(audioTrack, stream);
|
||||||
|
if (audioSender) {
|
||||||
|
console.log('[DesktopShare] ✅ 音频轨道添加成功');
|
||||||
|
} else {
|
||||||
|
console.warn('[DesktopShare] ⚠️ 音频轨道添加返回null');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[DesktopShare] ⚠️ 音频轨道添加失败,继续视频共享:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[DesktopShare] ℹ️ 未检测到音频轨道(这通常是正常的)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轨道添加完成,现在需要重新协商以包含媒体轨道
|
||||||
|
console.log('[DesktopShare] ✅ 桌面共享轨道添加完成,开始重新协商');
|
||||||
|
|
||||||
|
// 检查P2P连接是否已建立
|
||||||
|
if (!webRTC.isPeerConnected) {
|
||||||
|
console.error('[DesktopShare] ❌ P2P连接尚未建立,无法开始媒体传输');
|
||||||
|
throw new Error('P2P连接尚未建立');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的offer包含媒体轨道
|
||||||
|
console.log('[DesktopShare] 📨 创建包含媒体轨道的新offer进行重新协商');
|
||||||
|
const success = await webRTC.createOfferNow();
|
||||||
|
if (success) {
|
||||||
|
console.log('[DesktopShare] ✅ 媒体轨道重新协商成功');
|
||||||
|
} else {
|
||||||
|
console.error('[DesktopShare] ❌ 媒体轨道重新协商失败');
|
||||||
|
throw new Error('媒体轨道重新协商失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听流结束事件(用户停止共享)
|
||||||
|
const handleStreamEnded = () => {
|
||||||
|
console.log('[DesktopShare] 🛑 用户停止了屏幕共享');
|
||||||
|
stopSharing();
|
||||||
|
};
|
||||||
|
|
||||||
|
videoTrack?.addEventListener('ended', handleStreamEnded);
|
||||||
|
audioTrack?.addEventListener('ended', handleStreamEnded);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
videoTrack?.removeEventListener('ended', handleStreamEnded);
|
||||||
|
audioTrack?.removeEventListener('ended', handleStreamEnded);
|
||||||
|
};
|
||||||
|
}, [webRTC]);
|
||||||
|
|
||||||
|
// 处理远程流
|
||||||
|
const handleRemoteStream = useCallback((stream: MediaStream) => {
|
||||||
|
console.log('[DesktopShare] 收到远程流:', stream.getTracks().length, '个轨道');
|
||||||
|
updateState({ remoteStream: stream });
|
||||||
|
|
||||||
|
// 如果有视频元素引用,设置流
|
||||||
|
if (remoteVideoRef.current) {
|
||||||
|
remoteVideoRef.current.srcObject = stream;
|
||||||
|
}
|
||||||
|
}, [updateState]);
|
||||||
|
|
||||||
|
// 创建房间(只建立连接,等待对方加入)
|
||||||
|
const createRoom = useCallback(async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
updateState({ error: null, isWaitingForPeer: false });
|
||||||
|
|
||||||
|
// 生成房间代码
|
||||||
|
const roomCode = generateRoomCode();
|
||||||
|
console.log('[DesktopShare] 🚀 创建桌面共享房间,代码:', roomCode);
|
||||||
|
|
||||||
|
// 建立WebRTC连接(作为发送方)
|
||||||
|
console.log('[DesktopShare] 📡 正在建立WebRTC连接...');
|
||||||
|
await webRTC.connect(roomCode, 'sender');
|
||||||
|
console.log('[DesktopShare] ✅ WebSocket连接已建立');
|
||||||
|
|
||||||
|
updateState({
|
||||||
|
connectionCode: roomCode,
|
||||||
|
isWaitingForPeer: true, // 标记为等待对方连接
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DesktopShare] 🎯 房间创建完成,等待对方加入建立P2P连接');
|
||||||
|
return roomCode;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '创建房间失败';
|
||||||
|
console.error('[DesktopShare] ❌ 创建房间失败:', error);
|
||||||
|
updateState({ error: errorMessage, connectionCode: '', isWaitingForPeer: false });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [webRTC, generateRoomCode, updateState]);
|
||||||
|
|
||||||
|
// 开始桌面共享(在接收方加入后)
|
||||||
|
const startSharing = useCallback(async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// 检查WebSocket连接状态
|
||||||
|
if (!webRTC.isWebSocketConnected) {
|
||||||
|
throw new Error('WebSocket连接未建立,请先创建房间');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState({ error: null });
|
||||||
|
console.log('[DesktopShare] 📺 正在请求桌面共享权限...');
|
||||||
|
|
||||||
|
// 获取桌面流
|
||||||
|
const stream = await getDesktopStream();
|
||||||
|
localStreamRef.current = stream;
|
||||||
|
console.log('[DesktopShare] ✅ 桌面流获取成功');
|
||||||
|
|
||||||
|
// 设置视频发送(这会添加轨道并创建offer,启动P2P连接)
|
||||||
|
console.log('[DesktopShare] 📤 正在设置视频轨道推送并建立P2P连接...');
|
||||||
|
await setupVideoSending(stream);
|
||||||
|
console.log('[DesktopShare] ✅ 视频轨道推送设置完成');
|
||||||
|
|
||||||
|
updateState({
|
||||||
|
isSharing: true,
|
||||||
|
isWaitingForPeer: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DesktopShare] 🎉 桌面共享已开始');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '开始桌面共享失败';
|
||||||
|
console.error('[DesktopShare] ❌ 开始共享失败:', error);
|
||||||
|
updateState({ error: errorMessage, isSharing: false });
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
|
if (localStreamRef.current) {
|
||||||
|
localStreamRef.current.getTracks().forEach(track => track.stop());
|
||||||
|
localStreamRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [webRTC, getDesktopStream, setupVideoSending, updateState]);
|
||||||
|
|
||||||
|
// 切换桌面共享(重新选择屏幕)
|
||||||
|
const switchDesktop = useCallback(async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!webRTC.isPeerConnected) {
|
||||||
|
throw new Error('P2P连接未建立');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.isSharing) {
|
||||||
|
throw new Error('当前未在共享桌面');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState({ error: null });
|
||||||
|
console.log('[DesktopShare] 🔄 正在切换桌面共享...');
|
||||||
|
|
||||||
|
// 获取新的桌面流
|
||||||
|
const newStream = await getDesktopStream();
|
||||||
|
|
||||||
|
// 停止之前的流
|
||||||
|
if (localStreamRef.current) {
|
||||||
|
localStreamRef.current.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
localStreamRef.current = newStream;
|
||||||
|
console.log('[DesktopShare] ✅ 新桌面流获取成功');
|
||||||
|
|
||||||
|
// 设置新的视频发送
|
||||||
|
await setupVideoSending(newStream);
|
||||||
|
console.log('[DesktopShare] ✅ 桌面切换完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '切换桌面失败';
|
||||||
|
console.error('[DesktopShare] ❌ 切换桌面失败:', error);
|
||||||
|
updateState({ error: errorMessage });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [webRTC, state.isSharing, getDesktopStream, setupVideoSending, updateState]);
|
||||||
|
|
||||||
|
// 停止桌面共享
|
||||||
|
const stopSharing = useCallback(async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
console.log('[DesktopShare] 停止桌面共享');
|
||||||
|
|
||||||
|
// 停止本地流
|
||||||
|
if (localStreamRef.current) {
|
||||||
|
localStreamRef.current.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('[DesktopShare] 停止轨道:', track.kind);
|
||||||
|
});
|
||||||
|
localStreamRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除发送器
|
||||||
|
if (currentSenderRef.current) {
|
||||||
|
webRTC.removeTrack(currentSenderRef.current);
|
||||||
|
currentSenderRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断开WebRTC连接
|
||||||
|
webRTC.disconnect();
|
||||||
|
|
||||||
|
updateState({
|
||||||
|
isSharing: false,
|
||||||
|
connectionCode: '',
|
||||||
|
error: null,
|
||||||
|
isWaitingForPeer: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DesktopShare] 桌面共享已停止');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '停止桌面共享失败';
|
||||||
|
console.error('[DesktopShare] 停止共享失败:', error);
|
||||||
|
updateState({ error: errorMessage });
|
||||||
|
}
|
||||||
|
}, [webRTC, updateState]);
|
||||||
|
|
||||||
|
// 加入桌面共享观看
|
||||||
|
const joinSharing = useCallback(async (code: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
updateState({ error: null });
|
||||||
|
console.log('[DesktopShare] 🔍 正在加入桌面共享观看:', code);
|
||||||
|
|
||||||
|
// 连接WebRTC
|
||||||
|
console.log('[DesktopShare] 🔗 正在连接WebRTC作为接收方...');
|
||||||
|
await webRTC.connect(code, 'receiver');
|
||||||
|
console.log('[DesktopShare] ✅ WebRTC连接建立完成');
|
||||||
|
|
||||||
|
// 等待连接完全建立
|
||||||
|
console.log('[DesktopShare] ⏳ 等待连接稳定...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// 设置远程流处理 - 在连接建立后设置
|
||||||
|
console.log('[DesktopShare] 📡 设置远程流处理器...');
|
||||||
|
webRTC.onTrack((event: RTCTrackEvent) => {
|
||||||
|
console.log('[DesktopShare] 🎥 收到远程轨道:', event.track.kind, event.track.id);
|
||||||
|
console.log('[DesktopShare] 远程流数量:', event.streams.length);
|
||||||
|
|
||||||
|
if (event.streams.length > 0) {
|
||||||
|
const remoteStream = event.streams[0];
|
||||||
|
console.log('[DesktopShare] 🎬 设置远程流,轨道数量:', remoteStream.getTracks().length);
|
||||||
|
handleRemoteStream(remoteStream);
|
||||||
|
} else {
|
||||||
|
console.warn('[DesktopShare] ⚠️ 收到轨道但没有关联的流');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateState({ isViewing: true });
|
||||||
|
console.log('[DesktopShare] 👁️ 已进入桌面共享观看模式,等待接收流...');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '加入桌面共享失败';
|
||||||
|
console.error('[DesktopShare] ❌ 加入观看失败:', error);
|
||||||
|
updateState({ error: errorMessage, isViewing: false });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, [webRTC, handleRemoteStream, updateState]);
|
||||||
|
|
||||||
|
// 停止观看桌面共享
|
||||||
|
const stopViewing = useCallback(async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
console.log('[DesktopShare] 停止观看桌面共享');
|
||||||
|
|
||||||
|
// 断开WebRTC连接
|
||||||
|
webRTC.disconnect();
|
||||||
|
|
||||||
|
updateState({
|
||||||
|
isViewing: false,
|
||||||
|
remoteStream: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DesktopShare] 已停止观看桌面共享');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '停止观看失败';
|
||||||
|
console.error('[DesktopShare] 停止观看失败:', error);
|
||||||
|
updateState({ error: errorMessage });
|
||||||
|
}
|
||||||
|
}, [webRTC, updateState]);
|
||||||
|
|
||||||
|
// 设置远程视频元素引用
|
||||||
|
const setRemoteVideoRef = useCallback((videoElement: HTMLVideoElement | null) => {
|
||||||
|
remoteVideoRef.current = videoElement;
|
||||||
|
if (videoElement && state.remoteStream) {
|
||||||
|
videoElement.srcObject = state.remoteStream;
|
||||||
|
}
|
||||||
|
}, [state.remoteStream]);
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (localStreamRef.current) {
|
||||||
|
localStreamRef.current.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
isSharing: state.isSharing,
|
||||||
|
isViewing: state.isViewing,
|
||||||
|
connectionCode: state.connectionCode,
|
||||||
|
remoteStream: state.remoteStream,
|
||||||
|
error: state.error,
|
||||||
|
isWaitingForPeer: state.isWaitingForPeer,
|
||||||
|
isConnected: webRTC.isConnected,
|
||||||
|
isConnecting: webRTC.isConnecting,
|
||||||
|
isWebSocketConnected: webRTC.isWebSocketConnected,
|
||||||
|
isPeerConnected: webRTC.isPeerConnected,
|
||||||
|
// 新增:表示是否可以开始共享(WebSocket已连接且有房间代码)
|
||||||
|
canStartSharing: webRTC.isWebSocketConnected && !!state.connectionCode,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
createRoom, // 创建房间
|
||||||
|
startSharing, // 选择桌面并建立P2P连接
|
||||||
|
switchDesktop, // 新增:切换桌面
|
||||||
|
stopSharing,
|
||||||
|
joinSharing,
|
||||||
|
stopViewing,
|
||||||
|
setRemoteVideoRef,
|
||||||
|
|
||||||
|
// WebRTC连接状态
|
||||||
|
webRTCError: webRTC.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,10 @@ import type { WebRTCConnection } from './useSharedWebRTCManager';
|
|||||||
|
|
||||||
// 文件传输状态
|
// 文件传输状态
|
||||||
interface FileTransferState {
|
interface FileTransferState {
|
||||||
|
isConnecting: boolean;
|
||||||
|
isConnected: boolean;
|
||||||
|
isWebSocketConnected: boolean;
|
||||||
|
connectionError: string | null;
|
||||||
isTransferring: boolean;
|
isTransferring: boolean;
|
||||||
progress: number;
|
progress: number;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
@@ -50,6 +54,10 @@ const CHUNK_SIZE = 256 * 1024; // 256KB
|
|||||||
export function useFileTransferBusiness(connection: WebRTCConnection) {
|
export function useFileTransferBusiness(connection: WebRTCConnection) {
|
||||||
|
|
||||||
const [state, setState] = useState<FileTransferState>({
|
const [state, setState] = useState<FileTransferState>({
|
||||||
|
isConnecting: false,
|
||||||
|
isConnected: false,
|
||||||
|
isWebSocketConnected: false,
|
||||||
|
connectionError: null,
|
||||||
isTransferring: false,
|
isTransferring: false,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -177,6 +185,17 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
|
|||||||
};
|
};
|
||||||
}, [handleMessage, handleData]);
|
}, [handleMessage, handleData]);
|
||||||
|
|
||||||
|
// 监听连接状态变化 (直接使用 connection 的状态)
|
||||||
|
useEffect(() => {
|
||||||
|
// 同步连接状态
|
||||||
|
updateState({
|
||||||
|
isConnecting: connection.isConnecting,
|
||||||
|
isConnected: connection.isConnected,
|
||||||
|
isWebSocketConnected: connection.isWebSocketConnected,
|
||||||
|
connectionError: connection.error
|
||||||
|
});
|
||||||
|
}, [connection.isConnecting, connection.isConnected, connection.isWebSocketConnected, connection.error, updateState]);
|
||||||
|
|
||||||
// 连接
|
// 连接
|
||||||
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
|
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
|
||||||
return connection.connect(roomCode, role);
|
return connection.connect(roomCode, role);
|
||||||
@@ -263,6 +282,11 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
|
|||||||
|
|
||||||
// 发送文件列表
|
// 发送文件列表
|
||||||
const sendFileList = useCallback((fileList: FileInfo[]) => {
|
const sendFileList = useCallback((fileList: FileInfo[]) => {
|
||||||
|
if (!connection.isPeerConnected) {
|
||||||
|
console.log('P2P连接未建立,等待连接后再发送文件列表');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (connection.getChannelState() !== 'open') {
|
if (connection.getChannelState() !== 'open') {
|
||||||
console.error('数据通道未准备就绪,无法发送文件列表');
|
console.error('数据通道未准备就绪,无法发送文件列表');
|
||||||
return;
|
return;
|
||||||
@@ -313,13 +337,7 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 继承基础连接状态
|
// 文件传输状态(包括连接状态)
|
||||||
isConnected: connection.isConnected,
|
|
||||||
isConnecting: connection.isConnecting,
|
|
||||||
isWebSocketConnected: connection.isWebSocketConnected,
|
|
||||||
connectionError: connection.error,
|
|
||||||
|
|
||||||
// 文件传输状态
|
|
||||||
...state,
|
...state,
|
||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState, useRef, useCallback } from 'react';
|
import { useState, useRef, useCallback } from 'react';
|
||||||
import { config } from '@/lib/config';
|
import { getWsUrl } from '@/lib/config';
|
||||||
|
|
||||||
// 基础连接状态
|
// 基础连接状态
|
||||||
interface WebRTCState {
|
interface WebRTCState {
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
isConnecting: boolean;
|
isConnecting: boolean;
|
||||||
isWebSocketConnected: boolean;
|
isWebSocketConnected: boolean;
|
||||||
|
isPeerConnected: boolean; // 新增:P2P连接状态
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export interface WebRTCConnection {
|
|||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
isConnecting: boolean;
|
isConnecting: boolean;
|
||||||
isWebSocketConnected: boolean;
|
isWebSocketConnected: boolean;
|
||||||
|
isPeerConnected: boolean; // 新增:P2P连接状态
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
@@ -44,6 +46,13 @@ export interface WebRTCConnection {
|
|||||||
|
|
||||||
// 当前房间信息
|
// 当前房间信息
|
||||||
currentRoom: { code: string; role: 'sender' | 'receiver' } | null;
|
currentRoom: { code: string; role: 'sender' | 'receiver' } | null;
|
||||||
|
|
||||||
|
// 媒体轨道方法
|
||||||
|
addTrack: (track: MediaStreamTrack, stream: MediaStream) => RTCRtpSender | null;
|
||||||
|
removeTrack: (sender: RTCRtpSender) => void;
|
||||||
|
onTrack: (callback: (event: RTCTrackEvent) => void) => void;
|
||||||
|
getPeerConnection: () => RTCPeerConnection | null;
|
||||||
|
createOfferNow: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +64,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
isConnected: false,
|
isConnected: false,
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
isWebSocketConnected: false,
|
isWebSocketConnected: false,
|
||||||
|
isPeerConnected: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,12 +80,12 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
const messageHandlers = useRef<Map<string, MessageHandler>>(new Map());
|
const messageHandlers = useRef<Map<string, MessageHandler>>(new Map());
|
||||||
const dataHandlers = useRef<Map<string, DataHandler>>(new Map());
|
const dataHandlers = useRef<Map<string, DataHandler>>(new Map());
|
||||||
|
|
||||||
// STUN 服务器配置
|
// STUN 服务器配置 - 使用更稳定的服务器
|
||||||
const STUN_SERVERS = [
|
const STUN_SERVERS = [
|
||||||
{ urls: 'stun:stun.chat.bilibili.com' },
|
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
{ urls: 'stun:stun.miwifi.com' },
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||||
{ urls: 'stun:turn.cloudflare.com:3478' },
|
{ urls: 'stun:stun2.l.google.com:19302' },
|
||||||
|
{ urls: 'stun:global.stun.twilio.com:3478' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const updateState = useCallback((updates: Partial<WebRTCState>) => {
|
const updateState = useCallback((updates: Partial<WebRTCState>) => {
|
||||||
@@ -84,44 +94,47 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
|
|
||||||
// 清理连接
|
// 清理连接
|
||||||
const cleanup = useCallback(() => {
|
const cleanup = useCallback(() => {
|
||||||
// console.log('[SharedWebRTC] 清理连接');
|
console.log('[SharedWebRTC] 清理连接');
|
||||||
// if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
// clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
// timeoutRef.current = null;
|
timeoutRef.current = null;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (dcRef.current) {
|
if (dcRef.current) {
|
||||||
// dcRef.current.close();
|
dcRef.current.close();
|
||||||
// dcRef.current = null;
|
dcRef.current = null;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (pcRef.current) {
|
if (pcRef.current) {
|
||||||
// pcRef.current.close();
|
pcRef.current.close();
|
||||||
// pcRef.current = null;
|
pcRef.current = null;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (wsRef.current) {
|
if (wsRef.current) {
|
||||||
// wsRef.current.close();
|
wsRef.current.close();
|
||||||
// wsRef.current = null;
|
wsRef.current = null;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// currentRoom.current = null;
|
currentRoom.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 创建 Offer
|
// 创建 Offer
|
||||||
const createOffer = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => {
|
const createOffer = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('[SharedWebRTC] 🎬 开始创建offer,当前轨道数量:', pc.getSenders().length);
|
||||||
|
|
||||||
const offer = await pc.createOffer({
|
const offer = await pc.createOffer({
|
||||||
offerToReceiveAudio: false,
|
offerToReceiveAudio: true, // 改为true以支持音频接收
|
||||||
offerToReceiveVideo: false,
|
offerToReceiveVideo: true, // 改为true以支持视频接收
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[SharedWebRTC] 📝 Offer创建成功,设置本地描述...');
|
||||||
await pc.setLocalDescription(offer);
|
await pc.setLocalDescription(offer);
|
||||||
|
|
||||||
const iceTimeout = setTimeout(() => {
|
const iceTimeout = setTimeout(() => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
|
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
|
||||||
console.log('[SharedWebRTC] 发送 offer (超时发送)');
|
console.log('[SharedWebRTC] 📤 发送 offer (超时发送)');
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
@@ -129,7 +142,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
clearTimeout(iceTimeout);
|
clearTimeout(iceTimeout);
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
|
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
|
||||||
console.log('[SharedWebRTC] 发送 offer (ICE收集完成)');
|
console.log('[SharedWebRTC] 📤 发送 offer (ICE收集完成)');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pc.onicegatheringstatechange = () => {
|
pc.onicegatheringstatechange = () => {
|
||||||
@@ -137,13 +150,13 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
clearTimeout(iceTimeout);
|
clearTimeout(iceTimeout);
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
|
ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
|
||||||
console.log('[SharedWebRTC] 发送 offer (ICE收集完成)');
|
console.log('[SharedWebRTC] 📤 发送 offer (ICE收集完成)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SharedWebRTC] 创建 offer 失败:', error);
|
console.error('[SharedWebRTC] ❌ 创建 offer 失败:', error);
|
||||||
updateState({ error: '创建连接失败', isConnecting: false });
|
updateState({ error: '创建连接失败', isConnecting: false });
|
||||||
}
|
}
|
||||||
}, [updateState]);
|
}, [updateState]);
|
||||||
@@ -187,60 +200,24 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
|
|
||||||
// 连接到房间
|
// 连接到房间
|
||||||
const connect = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => {
|
const connect = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => {
|
||||||
console.log('[SharedWebRTC] 连接到房间:', roomCode, role);
|
console.log('[SharedWebRTC] 🚀 开始连接到房间:', roomCode, role);
|
||||||
|
|
||||||
// 检查是否已经连接到相同房间
|
|
||||||
if (currentRoom.current?.code === roomCode && currentRoom.current?.role === role) {
|
|
||||||
if (state.isConnected) {
|
|
||||||
console.log('[SharedWebRTC] 已连接到相同房间,复用连接');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state.isConnecting) {
|
|
||||||
console.log('[SharedWebRTC] 正在连接到相同房间,等待连接完成');
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const checkConnection = () => {
|
|
||||||
if (state.isConnected) {
|
|
||||||
resolve();
|
|
||||||
} else if (!state.isConnecting) {
|
|
||||||
reject(new Error('连接失败'));
|
|
||||||
} else {
|
|
||||||
setTimeout(checkConnection, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
checkConnection();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果要连接到不同房间,先断开当前连接
|
|
||||||
if (currentRoom.current && (currentRoom.current.code !== roomCode || currentRoom.current.role !== role)) {
|
|
||||||
console.log('[SharedWebRTC] 切换到新房间,断开当前连接');
|
|
||||||
cleanup();
|
|
||||||
updateState({
|
|
||||||
isConnected: false,
|
|
||||||
isConnecting: false,
|
|
||||||
isWebSocketConnected: false,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 如果正在连接中,避免重复连接
|
||||||
if (state.isConnecting) {
|
if (state.isConnecting) {
|
||||||
console.warn('[SharedWebRTC] 正在连接中,跳过重复连接请求');
|
console.warn('[SharedWebRTC] ⚠️ 正在连接中,跳过重复连接请求');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理之前的连接
|
||||||
cleanup();
|
cleanup();
|
||||||
currentRoom.current = { code: roomCode, role };
|
currentRoom.current = { code: roomCode, role };
|
||||||
updateState({ isConnecting: true, error: null });
|
updateState({ isConnecting: true, error: null });
|
||||||
|
|
||||||
// 设置连接超时
|
// 注意:不在这里设置超时,因为WebSocket连接很快,
|
||||||
timeoutRef.current = setTimeout(() => {
|
// WebRTC连接的建立是在后续添加轨道时进行的
|
||||||
console.warn('[SharedWebRTC] 连接超时');
|
|
||||||
updateState({ error: '连接超时,请检查网络状况或重新尝试', isConnecting: false });
|
|
||||||
cleanup();
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('[SharedWebRTC] 🔧 创建PeerConnection...');
|
||||||
// 创建 PeerConnection
|
// 创建 PeerConnection
|
||||||
const pc = new RTCPeerConnection({
|
const pc = new RTCPeerConnection({
|
||||||
iceServers: STUN_SERVERS,
|
iceServers: STUN_SERVERS,
|
||||||
@@ -248,69 +225,119 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
});
|
});
|
||||||
pcRef.current = pc;
|
pcRef.current = pc;
|
||||||
|
|
||||||
// 连接 WebSocket
|
// 连接 WebSocket - 使用动态URL
|
||||||
const wsUrl = config.api.wsUrl.replace('/ws/p2p', '/ws/webrtc');
|
const baseWsUrl = getWsUrl();
|
||||||
const ws = new WebSocket(`${wsUrl}?code=${roomCode}&role=${role}&channel=shared`);
|
if (!baseWsUrl) {
|
||||||
|
throw new Error('WebSocket URL未配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的WebSocket URL
|
||||||
|
const wsUrl = baseWsUrl.replace('/ws/p2p', `/ws/webrtc?code=${roomCode}&role=${role}&channel=shared`);
|
||||||
|
console.log('[SharedWebRTC] 🌐 连接WebSocket:', wsUrl);
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
// WebSocket 事件处理
|
// WebSocket 事件处理
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log('[SharedWebRTC] WebSocket 连接已建立');
|
console.log('[SharedWebRTC] ✅ WebSocket 连接已建立,房间准备就绪');
|
||||||
updateState({ isWebSocketConnected: true });
|
updateState({
|
||||||
|
isWebSocketConnected: true,
|
||||||
if (role === 'sender') {
|
isConnecting: false, // WebSocket连接成功即表示初始连接完成
|
||||||
createOffer(pc, ws);
|
isConnected: true // 可以开始后续操作
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = async (event) => {
|
ws.onmessage = async (event) => {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
console.log('[SharedWebRTC] 收到信令消息:', message.type);
|
console.log('[SharedWebRTC] 📨 收到信令消息:', message.type);
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
|
case 'peer-joined':
|
||||||
|
// 对方加入房间的通知
|
||||||
|
console.log('[SharedWebRTC] 👥 对方已加入房间,角色:', message.payload?.role);
|
||||||
|
if (role === 'sender' && message.payload?.role === 'receiver') {
|
||||||
|
console.log('[SharedWebRTC] 🚀 接收方已连接,发送方自动建立P2P连接');
|
||||||
|
updateState({ isPeerConnected: true }); // 标记对方已加入,可以开始P2P
|
||||||
|
|
||||||
|
// 发送方自动创建offer建立基础P2P连接
|
||||||
|
try {
|
||||||
|
console.log('[SharedWebRTC] 📡 自动创建基础P2P连接offer');
|
||||||
|
await createOffer(pc, ws);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SharedWebRTC] 自动创建基础P2P连接失败:', error);
|
||||||
|
}
|
||||||
|
} else if (role === 'receiver' && message.payload?.role === 'sender') {
|
||||||
|
console.log('[SharedWebRTC] 🚀 发送方已连接,接收方准备接收P2P连接');
|
||||||
|
updateState({ isPeerConnected: true }); // 标记对方已加入
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'offer':
|
case 'offer':
|
||||||
|
console.log('[SharedWebRTC] 📬 处理offer...');
|
||||||
if (pc.signalingState === 'stable') {
|
if (pc.signalingState === 'stable') {
|
||||||
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
||||||
|
console.log('[SharedWebRTC] ✅ 设置远程描述完成');
|
||||||
|
|
||||||
const answer = await pc.createAnswer();
|
const answer = await pc.createAnswer();
|
||||||
await pc.setLocalDescription(answer);
|
await pc.setLocalDescription(answer);
|
||||||
|
console.log('[SharedWebRTC] ✅ 创建并设置answer完成');
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'answer', payload: answer }));
|
ws.send(JSON.stringify({ type: 'answer', payload: answer }));
|
||||||
console.log('[SharedWebRTC] 发送 answer');
|
console.log('[SharedWebRTC] 📤 发送 answer');
|
||||||
|
} else {
|
||||||
|
console.warn('[SharedWebRTC] ⚠️ PeerConnection状态不是stable:', pc.signalingState);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'answer':
|
case 'answer':
|
||||||
|
console.log('[SharedWebRTC] 📬 处理answer...');
|
||||||
if (pc.signalingState === 'have-local-offer') {
|
if (pc.signalingState === 'have-local-offer') {
|
||||||
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
||||||
console.log('[SharedWebRTC] 处理 answer 完成');
|
console.log('[SharedWebRTC] ✅ answer 处理完成');
|
||||||
|
} else {
|
||||||
|
console.warn('[SharedWebRTC] ⚠️ PeerConnection状态不是have-local-offer:', pc.signalingState);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ice-candidate':
|
case 'ice-candidate':
|
||||||
if (message.payload && pc.remoteDescription) {
|
if (message.payload && pc.remoteDescription) {
|
||||||
await pc.addIceCandidate(new RTCIceCandidate(message.payload));
|
try {
|
||||||
console.log('[SharedWebRTC] 添加 ICE 候选');
|
await pc.addIceCandidate(new RTCIceCandidate(message.payload));
|
||||||
|
console.log('[SharedWebRTC] ✅ 添加 ICE 候选成功');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[SharedWebRTC] ⚠️ 添加 ICE 候选失败:', err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[SharedWebRTC] ⚠️ ICE候选无效或远程描述未设置');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
console.error('[SharedWebRTC] 信令错误:', message.error);
|
console.error('[SharedWebRTC] ❌ 信令服务器错误:', message.error);
|
||||||
updateState({ error: message.error, isConnecting: false });
|
updateState({ error: message.error, isConnecting: false });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn('[SharedWebRTC] ⚠️ 未知消息类型:', message.type);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SharedWebRTC] 处理信令消息失败:', error);
|
console.error('[SharedWebRTC] ❌ 处理信令消息失败:', error);
|
||||||
|
updateState({ error: '信令处理失败: ' + error, isConnecting: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error('[SharedWebRTC] WebSocket 错误:', error);
|
console.error('[SharedWebRTC] ❌ WebSocket 错误:', error);
|
||||||
updateState({ error: 'WebSocket连接失败,请检查网络连接', isConnecting: false });
|
updateState({ error: 'WebSocket连接失败,请检查服务器是否运行在8080端口', isConnecting: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = (event) => {
|
||||||
console.log('[SharedWebRTC] WebSocket 连接已关闭');
|
console.log('[SharedWebRTC] 🔌 WebSocket 连接已关闭, 代码:', event.code, '原因:', event.reason);
|
||||||
updateState({ isWebSocketConnected: false });
|
updateState({ isWebSocketConnected: false });
|
||||||
|
if (event.code !== 1000 && event.code !== 1001) { // 非正常关闭
|
||||||
|
updateState({ error: `WebSocket异常关闭 (${event.code}): ${event.reason || '未知原因'}`, isConnecting: false });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// PeerConnection 事件处理
|
// PeerConnection 事件处理
|
||||||
@@ -320,32 +347,63 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
type: 'ice-candidate',
|
type: 'ice-candidate',
|
||||||
payload: event.candidate
|
payload: event.candidate
|
||||||
}));
|
}));
|
||||||
console.log('[SharedWebRTC] 发送 ICE 候选');
|
console.log('[SharedWebRTC] 📤 发送 ICE 候选:', event.candidate.candidate.substring(0, 50) + '...');
|
||||||
|
} else if (!event.candidate) {
|
||||||
|
console.log('[SharedWebRTC] 🏁 ICE 收集完成');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pc.oniceconnectionstatechange = () => {
|
||||||
|
console.log('[SharedWebRTC] 🧊 ICE连接状态变化:', pc.iceConnectionState);
|
||||||
|
switch (pc.iceConnectionState) {
|
||||||
|
case 'checking':
|
||||||
|
console.log('[SharedWebRTC] 🔍 正在检查ICE连接...');
|
||||||
|
break;
|
||||||
|
case 'connected':
|
||||||
|
case 'completed':
|
||||||
|
console.log('[SharedWebRTC] ✅ ICE连接成功');
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
console.error('[SharedWebRTC] ❌ ICE连接失败');
|
||||||
|
updateState({ error: 'ICE连接失败,可能是网络防火墙阻止了连接', isConnecting: false });
|
||||||
|
break;
|
||||||
|
case 'disconnected':
|
||||||
|
console.log('[SharedWebRTC] 🔌 ICE连接断开');
|
||||||
|
break;
|
||||||
|
case 'closed':
|
||||||
|
console.log('[SharedWebRTC] 🚫 ICE连接已关闭');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pc.onconnectionstatechange = () => {
|
pc.onconnectionstatechange = () => {
|
||||||
console.log('[SharedWebRTC] 连接状态变化:', pc.connectionState);
|
console.log('[SharedWebRTC] 🔗 WebRTC连接状态变化:', pc.connectionState);
|
||||||
switch (pc.connectionState) {
|
switch (pc.connectionState) {
|
||||||
|
case 'connecting':
|
||||||
|
console.log('[SharedWebRTC] 🔄 WebRTC正在连接中...');
|
||||||
|
updateState({ isPeerConnected: false });
|
||||||
|
break;
|
||||||
case 'connected':
|
case 'connected':
|
||||||
if (timeoutRef.current) {
|
console.log('[SharedWebRTC] 🎉 WebRTC P2P连接已完全建立,可以进行媒体传输');
|
||||||
clearTimeout(timeoutRef.current);
|
updateState({ isPeerConnected: true, error: null });
|
||||||
timeoutRef.current = null;
|
|
||||||
}
|
|
||||||
updateState({ isConnected: true, isConnecting: false, error: null });
|
|
||||||
break;
|
break;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
updateState({ error: 'WebRTC连接失败,可能是网络防火墙阻止了连接', isConnecting: false, isConnected: false });
|
// 只有在数据通道也未打开的情况下才认为连接真正失败
|
||||||
break;
|
const currentDc = dcRef.current;
|
||||||
case 'disconnected':
|
if (!currentDc || currentDc.readyState !== 'open') {
|
||||||
updateState({ isConnected: false });
|
console.error('[SharedWebRTC] ❌ WebRTC连接失败,数据通道未建立');
|
||||||
if (timeoutRef.current) {
|
updateState({ error: 'WebRTC连接失败,请检查网络设置或重试', isPeerConnected: false });
|
||||||
clearTimeout(timeoutRef.current);
|
} else {
|
||||||
timeoutRef.current = null;
|
console.log('[SharedWebRTC] ⚠️ WebRTC连接状态为failed,但数据通道正常,忽略此状态');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'disconnected':
|
||||||
|
console.log('[SharedWebRTC] 🔌 WebRTC连接已断开');
|
||||||
|
updateState({ isPeerConnected: false });
|
||||||
|
break;
|
||||||
case 'closed':
|
case 'closed':
|
||||||
updateState({ isConnected: false, isConnecting: false });
|
console.log('[SharedWebRTC] 🚫 WebRTC连接已关闭');
|
||||||
|
updateState({ isPeerConnected: false });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -360,6 +418,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
|
|
||||||
dataChannel.onopen = () => {
|
dataChannel.onopen = () => {
|
||||||
console.log('[SharedWebRTC] 数据通道已打开 (发送方)');
|
console.log('[SharedWebRTC] 数据通道已打开 (发送方)');
|
||||||
|
updateState({ isPeerConnected: true, error: null, isConnecting: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
dataChannel.onmessage = handleDataChannelMessage;
|
dataChannel.onmessage = handleDataChannelMessage;
|
||||||
@@ -375,6 +434,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
|
|
||||||
dataChannel.onopen = () => {
|
dataChannel.onopen = () => {
|
||||||
console.log('[SharedWebRTC] 数据通道已打开 (接收方)');
|
console.log('[SharedWebRTC] 数据通道已打开 (接收方)');
|
||||||
|
updateState({ isPeerConnected: true, error: null, isConnecting: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
dataChannel.onmessage = handleDataChannelMessage;
|
dataChannel.onmessage = handleDataChannelMessage;
|
||||||
@@ -386,6 +446,19 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置轨道接收处理(对于接收方)
|
||||||
|
pc.ontrack = (event) => {
|
||||||
|
console.log('[SharedWebRTC] 🎥 PeerConnection收到轨道:', event.track.kind, event.track.id);
|
||||||
|
console.log('[SharedWebRTC] 关联的流数量:', event.streams.length);
|
||||||
|
|
||||||
|
if (event.streams.length > 0) {
|
||||||
|
console.log('[SharedWebRTC] 🎬 轨道关联到流:', event.streams[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里不处理,让具体的业务逻辑处理
|
||||||
|
// onTrack会被业务逻辑重新设置
|
||||||
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SharedWebRTC] 连接失败:', error);
|
console.error('[SharedWebRTC] 连接失败:', error);
|
||||||
updateState({
|
updateState({
|
||||||
@@ -403,6 +476,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
isConnected: false,
|
isConnected: false,
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
isWebSocketConnected: false,
|
isWebSocketConnected: false,
|
||||||
|
isPeerConnected: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
}, [cleanup]);
|
}, [cleanup]);
|
||||||
@@ -478,11 +552,90 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
state.isConnected;
|
state.isConnected;
|
||||||
}, [state.isConnected]);
|
}, [state.isConnected]);
|
||||||
|
|
||||||
|
// 添加媒体轨道
|
||||||
|
const addTrack = useCallback((track: MediaStreamTrack, stream: MediaStream) => {
|
||||||
|
const pc = pcRef.current;
|
||||||
|
if (!pc) {
|
||||||
|
console.error('[SharedWebRTC] PeerConnection 不可用');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return pc.addTrack(track, stream);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SharedWebRTC] 添加轨道失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 移除媒体轨道
|
||||||
|
const removeTrack = useCallback((sender: RTCRtpSender) => {
|
||||||
|
const pc = pcRef.current;
|
||||||
|
if (!pc) {
|
||||||
|
console.error('[SharedWebRTC] PeerConnection 不可用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pc.removeTrack(sender);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SharedWebRTC] 移除轨道失败:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 设置轨道处理器
|
||||||
|
const onTrack = useCallback((handler: (event: RTCTrackEvent) => void) => {
|
||||||
|
const pc = pcRef.current;
|
||||||
|
if (!pc) {
|
||||||
|
console.warn('[SharedWebRTC] PeerConnection 尚未准备就绪,将在连接建立后设置onTrack');
|
||||||
|
// 延迟设置,等待PeerConnection准备就绪
|
||||||
|
const checkAndSetTrackHandler = () => {
|
||||||
|
const currentPc = pcRef.current;
|
||||||
|
if (currentPc) {
|
||||||
|
console.log('[SharedWebRTC] ✅ PeerConnection 已准备就绪,设置onTrack处理器');
|
||||||
|
currentPc.ontrack = handler;
|
||||||
|
} else {
|
||||||
|
console.log('[SharedWebRTC] ⏳ 等待PeerConnection准备就绪...');
|
||||||
|
setTimeout(checkAndSetTrackHandler, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkAndSetTrackHandler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[SharedWebRTC] ✅ 立即设置onTrack处理器');
|
||||||
|
pc.ontrack = handler;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 获取PeerConnection实例
|
||||||
|
const getPeerConnection = useCallback(() => {
|
||||||
|
return pcRef.current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 立即创建offer(用于媒体轨道添加后的重新协商)
|
||||||
|
const createOfferNow = useCallback(async () => {
|
||||||
|
const pc = pcRef.current;
|
||||||
|
const ws = wsRef.current;
|
||||||
|
if (!pc || !ws) {
|
||||||
|
console.error('[SharedWebRTC] PeerConnection 或 WebSocket 不可用');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createOffer(pc, ws);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SharedWebRTC] 创建 offer 失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [createOffer]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
isConnected: state.isConnected,
|
isConnected: state.isConnected,
|
||||||
isConnecting: state.isConnecting,
|
isConnecting: state.isConnecting,
|
||||||
isWebSocketConnected: state.isWebSocketConnected,
|
isWebSocketConnected: state.isWebSocketConnected,
|
||||||
|
isPeerConnected: state.isPeerConnected,
|
||||||
error: state.error,
|
error: state.error,
|
||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
@@ -499,6 +652,13 @@ export function useSharedWebRTCManager(): WebRTCConnection {
|
|||||||
getChannelState,
|
getChannelState,
|
||||||
isConnectedToRoom,
|
isConnectedToRoom,
|
||||||
|
|
||||||
|
// 媒体轨道方法
|
||||||
|
addTrack,
|
||||||
|
removeTrack,
|
||||||
|
onTrack,
|
||||||
|
getPeerConnection,
|
||||||
|
createOfferNow,
|
||||||
|
|
||||||
// 当前房间信息
|
// 当前房间信息
|
||||||
currentRoom: currentRoom.current,
|
currentRoom: currentRoom.current,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,9 +84,14 @@ export function useTextTransferBusiness(connection: WebRTCConnection) {
|
|||||||
|
|
||||||
// 监听连接状态变化 (直接使用 connection 的状态)
|
// 监听连接状态变化 (直接使用 connection 的状态)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 这里我们直接依赖 connection 的状态变化
|
// 同步连接状态
|
||||||
// 由于我们使用共享连接,状态会自动同步
|
updateState({
|
||||||
}, []);
|
isConnecting: connection.isConnecting,
|
||||||
|
isConnected: connection.isConnected,
|
||||||
|
isWebSocketConnected: connection.isWebSocketConnected,
|
||||||
|
connectionError: connection.error
|
||||||
|
});
|
||||||
|
}, [connection.isConnecting, connection.isConnected, connection.isWebSocketConnected, connection.error, updateState]);
|
||||||
|
|
||||||
// 连接
|
// 连接
|
||||||
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
|
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
|
||||||
@@ -100,28 +105,32 @@ export function useTextTransferBusiness(connection: WebRTCConnection) {
|
|||||||
|
|
||||||
// 发送实时文本同步 (替代原来的 sendMessage)
|
// 发送实时文本同步 (替代原来的 sendMessage)
|
||||||
const sendTextSync = useCallback((text: string) => {
|
const sendTextSync = useCallback((text: string) => {
|
||||||
if (!connection) return;
|
if (!connection || !connection.isPeerConnected) return;
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
type: 'text-sync',
|
type: 'text-sync',
|
||||||
payload: { text }
|
payload: { text }
|
||||||
};
|
};
|
||||||
|
|
||||||
connection.sendMessage(message, CHANNEL_NAME);
|
const success = connection.sendMessage(message, CHANNEL_NAME);
|
||||||
console.log('发送实时文本同步:', text.length, '字符');
|
if (success) {
|
||||||
|
console.log('发送实时文本同步:', text.length, '字符');
|
||||||
|
}
|
||||||
}, [connection]);
|
}, [connection]);
|
||||||
|
|
||||||
// 发送打字状态
|
// 发送打字状态
|
||||||
const sendTypingStatus = useCallback((isTyping: boolean) => {
|
const sendTypingStatus = useCallback((isTyping: boolean) => {
|
||||||
if (!connection) return;
|
if (!connection || !connection.isPeerConnected) return;
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
type: 'text-typing',
|
type: 'text-typing',
|
||||||
payload: { typing: isTyping }
|
payload: { typing: isTyping }
|
||||||
};
|
};
|
||||||
|
|
||||||
connection.sendMessage(message, CHANNEL_NAME);
|
const success = connection.sendMessage(message, CHANNEL_NAME);
|
||||||
console.log('发送打字状态:', isTyping);
|
if (success) {
|
||||||
|
console.log('发送打字状态:', isTyping);
|
||||||
|
}
|
||||||
}, [connection]);
|
}, [connection]);
|
||||||
|
|
||||||
// 设置文本同步回调
|
// 设置文本同步回调
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const getCurrentBaseUrl = () => {
|
|||||||
return 'http://localhost:8080';
|
return 'http://localhost:8080';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 动态获取 WebSocket URL
|
// 动态获取 WebSocket URL - 总是在客户端运行时计算
|
||||||
const getCurrentWsUrl = () => {
|
const getCurrentWsUrl = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// 检查是否是 Next.js 开发服务器(端口 3000 或 3001)
|
// 检查是否是 Next.js 开发服务器(端口 3000 或 3001)
|
||||||
@@ -40,8 +40,8 @@ const getCurrentWsUrl = () => {
|
|||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
return `${protocol}//${window.location.host}/ws/p2p`;
|
return `${protocol}//${window.location.host}/ws/p2p`;
|
||||||
}
|
}
|
||||||
// 服务器端默认值
|
// 服务器端返回空字符串,强制在客户端计算
|
||||||
return 'ws://localhost:8080/ws/p2p';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
@@ -61,8 +61,8 @@ export const config = {
|
|||||||
// 直接后端URL (客户端在静态模式下使用) - 如果环境变量为空,则使用当前域名
|
// 直接后端URL (客户端在静态模式下使用) - 如果环境变量为空,则使用当前域名
|
||||||
directBackendUrl: getEnv('NEXT_PUBLIC_BACKEND_URL') || getCurrentBaseUrl(),
|
directBackendUrl: getEnv('NEXT_PUBLIC_BACKEND_URL') || getCurrentBaseUrl(),
|
||||||
|
|
||||||
// WebSocket地址 - 如果环境变量为空,则使用当前域名构建
|
// WebSocket地址 - 在客户端运行时动态计算,不在构建时预设
|
||||||
wsUrl: getEnv('NEXT_PUBLIC_WS_URL') || getCurrentWsUrl(),
|
wsUrl: '', // 将通过 getWsUrl() 函数动态获取
|
||||||
},
|
},
|
||||||
|
|
||||||
// 超时配置
|
// 超时配置
|
||||||
@@ -113,12 +113,23 @@ export function getDirectBackendUrl(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取WebSocket URL
|
* 获取WebSocket URL - 总是在客户端运行时动态计算
|
||||||
* @returns WebSocket连接地址
|
* @returns WebSocket连接地址
|
||||||
*/
|
*/
|
||||||
export function getWsUrl(): string {
|
export function getWsUrl(): string {
|
||||||
// 实时获取当前域名构建的 WebSocket URL
|
// 优先使用环境变量
|
||||||
return getEnv('NEXT_PUBLIC_WS_URL') || getCurrentWsUrl()
|
const envWsUrl = getEnv('NEXT_PUBLIC_WS_URL');
|
||||||
|
if (envWsUrl) {
|
||||||
|
return envWsUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是服务器端(SSG构建时),返回空字符串
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户端运行时动态计算
|
||||||
|
return getCurrentWsUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
66
chuan-next/src/styles/animations.css
Normal file
66
chuan-next/src/styles/animations.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/* 动画样式 */
|
||||||
|
@keyframes fade-in-up {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fade-in-up 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(156, 163, 175, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(156, 163, 175, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全屏时隐藏鼠标(桌面共享专用) */
|
||||||
|
.cursor-none {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-none:hover {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面共享控制栏过渡 */
|
||||||
|
.desktop-controls-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-controls-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition: opacity 300ms ease-in-out, transform 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-controls-exit {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-controls-exit-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: opacity 300ms ease-in-out, transform 300ms ease-in-out;
|
||||||
|
}
|
||||||
@@ -138,8 +138,33 @@ func (ws *WebRTCService) addClientToRoom(code string, client *WebRTCClient) {
|
|||||||
|
|
||||||
if client.Role == "sender" {
|
if client.Role == "sender" {
|
||||||
room.Sender = client
|
room.Sender = client
|
||||||
|
// 如果发送方连接,检查是否有接收方在等待,通知接收方
|
||||||
|
if room.Receiver != nil {
|
||||||
|
log.Printf("通知接收方:发送方已连接")
|
||||||
|
peerJoinedMsg := &WebRTCMessage{
|
||||||
|
Type: "peer-joined",
|
||||||
|
From: client.ID,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"role": "sender",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
room.Receiver.Connection.WriteJSON(peerJoinedMsg)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
room.Receiver = client
|
room.Receiver = client
|
||||||
|
// 如果接收方连接,通知发送方可以开始建立P2P连接
|
||||||
|
if room.Sender != nil {
|
||||||
|
log.Printf("通知发送方:接收方已连接,可以开始建立P2P连接")
|
||||||
|
peerJoinedMsg := &WebRTCMessage{
|
||||||
|
Type: "peer-joined",
|
||||||
|
From: client.ID,
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"role": "receiver",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
room.Sender.Connection.WriteJSON(peerJoinedMsg)
|
||||||
|
}
|
||||||
|
|
||||||
// 如果接收方连接,且有保存的offer,立即发送给接收方
|
// 如果接收方连接,且有保存的offer,立即发送给接收方
|
||||||
if room.LastOffer != nil {
|
if room.LastOffer != nil {
|
||||||
log.Printf("向新连接的接收方发送保存的offer")
|
log.Printf("向新连接的接收方发送保存的offer")
|
||||||
|
|||||||
Reference in New Issue
Block a user