diff --git a/chuan-next/src/app/HomePage.tsx b/chuan-next/src/app/HomePage.tsx index 171cb1e..d221efb 100644 --- a/chuan-next/src/app/HomePage.tsx +++ b/chuan-next/src/app/HomePage.tsx @@ -3,10 +3,11 @@ import React, { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; 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 { WebRTCFileTransfer } from '@/components/WebRTCFileTransfer'; -import {WebRTCTextImageTransfer} from '@/components/WebRTCTextImageTransfer'; +import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer'; +import DesktopShare from '@/components/DesktopShare'; export default function HomePage() { const searchParams = useSearchParams(); @@ -73,13 +74,12 @@ export default function HomePage() { 共享桌面 桌面 - 开发中 - + @@ -94,23 +94,7 @@ export default function HomePage() { -
-
-
- -
-

桌面共享

-

此功能正在开发中...

-
-

- 🚧 敬请期待!我们正在为您开发实时桌面共享功能 -

-
-

- 目前请使用文件传输功能 -

-
-
+
diff --git a/chuan-next/src/app/globals.css b/chuan-next/src/app/globals.css index 3e7fab7..725d87c 100644 --- a/chuan-next/src/app/globals.css +++ b/chuan-next/src/app/globals.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "../styles/animations.css"; :root { --background: 0 0% 100%; diff --git a/chuan-next/src/components/DesktopShare.tsx b/chuan-next/src/components/DesktopShare.tsx index f7b022e..879bb5e 100644 --- a/chuan-next/src/components/DesktopShare.tsx +++ b/chuan-next/src/components/DesktopShare.tsx @@ -4,33 +4,41 @@ import React, { useState, useCallback, useEffect } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; 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 { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness'; +import DesktopViewer from '@/components/DesktopViewer'; +import QRCodeDisplay from '@/components/QRCodeDisplay'; interface DesktopShareProps { - onStartSharing?: () => Promise; // 返回连接码 + // 保留向后兼容性的props + onStartSharing?: () => Promise; onStopSharing?: () => Promise; onJoinSharing?: (code: string) => Promise; } -export default function DesktopShare({ onStartSharing, onStopSharing, onJoinSharing }: DesktopShareProps) { +export default function DesktopShare({ + onStartSharing, + onStopSharing, + onJoinSharing +}: DesktopShareProps) { const searchParams = useSearchParams(); const router = useRouter(); const [mode, setMode] = useState<'share' | 'view'>('share'); - const [connectionCode, setConnectionCode] = useState(''); const [inputCode, setInputCode] = useState(''); - const [isSharing, setIsSharing] = useState(false); - const [isViewing, setIsViewing] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [showDebug, setShowDebug] = useState(false); const { showToast } = useToast(); + // 使用桌面共享业务逻辑 + const desktopShare = useDesktopShareBusiness(); + // 从URL参数中获取初始模式 useEffect(() => { const urlMode = searchParams.get('mode'); const type = searchParams.get('type'); if (type === 'desktop' && urlMode) { - // 将send映射为share,receive映射为view if (urlMode === 'send') { setMode('share'); } else if (urlMode === 'receive') { @@ -42,75 +50,151 @@ export default function DesktopShare({ onStartSharing, onStopSharing, onJoinShar // 更新URL参数 const updateMode = useCallback((newMode: 'share' | 'view') => { setMode(newMode); - const params = new URLSearchParams(searchParams.toString()); - params.set('type', 'desktop'); - // 将share映射为send,view映射为receive以保持一致性 - params.set('mode', newMode === 'share' ? 'send' : 'receive'); - router.push(`?${params.toString()}`, { scroll: false }); - }, [searchParams, router]); + const currentUrl = new URL(window.location.href); + currentUrl.searchParams.set('type', 'desktop'); + currentUrl.searchParams.set('mode', newMode === 'share' ? 'send' : 'receive'); + router.replace(currentUrl.pathname + currentUrl.search); + }, [router]); - const handleStartSharing = useCallback(async () => { - if (!onStartSharing) return; - - setIsLoading(true); + // 复制房间代码 + const copyCode = useCallback(async (code: string) => { try { - const code = await onStartSharing(); - setConnectionCode(code); - setIsSharing(true); - showToast('桌面共享已开始!', 'success'); + await navigator.clipboard.writeText(code); + showToast('房间代码已复制到剪贴板', 'success'); } catch (error) { - console.error('开始共享失败:', 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'); + console.error('复制失败:', error); + showToast('复制失败,请手动复制', 'error'); } }, [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 (
- {/* 模式切换 */} + {/* 模式选择器 */}
+
+
+ ) : ( + // 房间已创建,显示取件码和等待界面 +
+ {/* 功能标题和状态 */} +
+
+
+ +
+
+

共享桌面

+

+ {desktopShare.isPeerConnected ? '✅ 接收方已连接,现在可以开始共享桌面' : + desktopShare.isWebSocketConnected ? '⏳ 房间已创建,等待接收方加入建立P2P连接' : + '⚠️ 等待连接'} +

+
+
+ + {/* 竖线分割 */} +
+ + {/* 状态显示 */} +
+
连接状态
+
+ {/* WebSocket状态 */} +
+
+ WS +
+ + {/* 分隔符 */} +
|
+ + {/* WebRTC状态 */} +
+
+ RTC +
+
- {isSharing && connectionCode && ( -
- {connectionCode} + + {/* 桌面共享控制区域 */} + {desktopShare.canStartSharing && ( +
+
+

+ + 桌面共享控制 +

+ {desktopShare.isSharing && ( +
+
+ 共享中 +
+ )} +
+ +
+ {!desktopShare.isSharing ? ( +
+ + + {!desktopShare.isPeerConnected && ( +
+

+ 等待接收方加入房间建立P2P连接... +

+
+
+ 正在等待连接 +
+
+ )} +
+ ) : ( +
+
+ + 桌面共享进行中 +
+
+ + +
+
+ )} +
)} -
-
-
- {!isSharing ? ( - - ) : ( -
-
-
-

连接码

-
{connectionCode}
+ {/* 取件码显示 - 和文件传输一致的风格 */} +
+ {/* 左上角状态提示 */} +
+
+
+ +
+
+

房间码生成成功!

+

分享以下信息给观看方

+
+
+
+ + {/* 中间区域:取件码 + 分隔线 + 二维码 */} +
+ {/* 左侧:取件码 */} +
+ +
+
+ {desktopShare.connectionCode} +
+
+
+ + {/* 分隔线 - 大屏幕显示竖线,移动端隐藏 */} +
+ + {/* 右侧:二维码 */} +
+ +
+ +
+
+ 使用手机扫码快速观看 +
+
+
+ + {/* 底部:观看链接 */} +
+
+
+
+ {`${typeof window !== 'undefined' ? window.location.origin : ''}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`} +
+
+ +
+
+
+
+ )} +
+ ) : ( + /* 观看模式 */ +
+
+ {!desktopShare.isViewing ? ( + // 输入房间代码界面 - 与文本消息风格一致 +
+
+
+
+ +
+
+

输入房间代码

+

请输入6位房间代码来观看桌面共享

+
+
+
+ +
{ e.preventDefault(); handleJoinViewing(); }} className="space-y-4 sm:space-y-6"> +
+
+ 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} + /> +
+

+ {inputCode.length}/6 位 +

+
+ +
+ +
+
+
+ ) : ( + // 已连接,显示桌面观看界面 +
+
+
+
+ +
+
+

桌面观看

+

+ ✅ 已连接,正在观看桌面共享 +

+
+
+
+ + {/* 连接成功状态 */} +
+

已连接到桌面共享房间

+

房间代码: {inputCode}

+
+ + {/* 观看中的控制面板 */} +
+
+
+ + 观看中 +
+
- -
- )} -
-
- ) : ( -
- {/* 功能标题和状态 */} -
-
-
- -
-
-

观看桌面

-

- {isViewing ? '正在观看桌面共享' : '输入连接码观看他人的桌面'} -

-
-
- - {/* 竖线分割 */} -
- - {/* 状态显示 */} -
-
连接状态
-
- {/* WebSocket状态 */} -
-
- WS -
- - {/* 分隔符 */} -
|
- - {/* WebRTC状态 */} -
- {isViewing ? ( - <> -
- RTC - - ) : isLoading ? ( - <> -
- RTC - - ) : ( - <> -
- RTC - - )} -
-
- {isViewing && ( -
- 观看中 -
- )} -
-
- -
- {!isViewing ? ( - <> - 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} - /> - - - - ) : ( -
-
-
- -

桌面共享画面

+ {/* 桌面显示区域 */} + {desktopShare.remoteStream ? ( + + ) : ( +
+
+ +

等待接收桌面画面...

+

发送方开始共享后,桌面画面将在这里显示

+ +
+
+ 等待桌面流... +
+
-
- - + )}
)}
)} + + {/* 错误显示 */} + {desktopShare.error && ( +
+

{desktopShare.error}

+
+ )} + + {/* 调试信息 */} +
+ + + {showDebug && ( +
+
WebSocket连接: {desktopShare.isWebSocketConnected ? '✅' : '❌'}
+
P2P连接: {desktopShare.isPeerConnected ? '✅' : '❌'}
+
房间代码: {desktopShare.connectionCode || '未创建'}
+
共享状态: {desktopShare.isSharing ? '进行中' : '未共享'}
+
观看状态: {desktopShare.isViewing ? '观看中' : '未观看'}
+
等待对方: {desktopShare.isWaitingForPeer ? '是' : '否'}
+
远程流: {desktopShare.remoteStream ? '已接收' : '无'}
+
+ )} +
); } diff --git a/chuan-next/src/components/DesktopViewer.tsx b/chuan-next/src/components/DesktopViewer.tsx new file mode 100644 index 0000000..867845c --- /dev/null +++ b/chuan-next/src/components/DesktopViewer.tsx @@ -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(null); + const containerRef = useRef(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(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 ( +
+ +

+ {isConnected ? '等待桌面共享流...' : '等待桌面共享连接...'} +

+ {connectionCode && ( +

连接码: {connectionCode}

+ )} +
+
+ {isConnected ? '已连接,等待视频流' : '正在建立连接'} +
+
+ ); + } + + return ( +
isFullscreen && setShowControls(true)} + > + {/* 主视频显示 */} +
+ ); +} diff --git a/chuan-next/src/components/WebRTCFileTransfer.tsx b/chuan-next/src/components/WebRTCFileTransfer.tsx index a478bd9..fe07ece 100644 --- a/chuan-next/src/components/WebRTCFileTransfer.tsx +++ b/chuan-next/src/components/WebRTCFileTransfer.tsx @@ -287,8 +287,8 @@ export const WebRTCFileTransfer: React.FC = () => { const updatedList = [...prev, ...newFileInfos]; console.log('更新后的文件列表:', updatedList); - // 如果已连接,立即同步文件列表 - if (isConnected && pickupCode) { + // 如果P2P连接已建立,立即同步文件列表 + if (isConnected && connection.isPeerConnected && pickupCode) { console.log('立即同步文件列表到对端'); setTimeout(() => sendFileList(updatedList), 100); } @@ -573,18 +573,23 @@ export const WebRTCFileTransfer: React.FC = () => { console.log('WebRTC连接状态:', isConnected); console.log('连接中状态:', isConnecting); - // 如果WebSocket断开但不是主动断开的情况 + // 只有在之前已经建立过连接,现在断开的情况下才显示断开提示 + // 避免在初始连接时误报断开 if (!isWebSocketConnected && !isConnected && !isConnecting && pickupCode) { - showToast('与服务器的连接已断开,请重新连接', "error"); - - // 清理传输状态 - console.log('WebSocket断开,清理传输状态'); - setCurrentTransferFile(null); - setFileList(prev => prev.map(item => - item.status === 'downloading' - ? { ...item, status: 'ready' as const, progress: 0 } - : item - )); + // 增加额外检查:只有在之前曾经连接成功过的情况下才显示断开提示 + // 通过检查是否有文件列表来判断是否曾经连接过 + if (fileList.length > 0 || currentTransferFile) { + showToast('与服务器的连接已断开,请重新连接', "error"); + + // 清理传输状态 + console.log('WebSocket断开,清理传输状态'); + setCurrentTransferFile(null); + setFileList(prev => prev.map(item => + item.status === 'downloading' + ? { ...item, status: 'ready' as const, progress: 0 } + : item + )); + } } // WebSocket连接成功时的提示 @@ -592,7 +597,7 @@ export const WebRTCFileTransfer: React.FC = () => { console.log('WebSocket已连接,正在建立P2P连接...'); } - }, [isWebSocketConnected, isConnected, isConnecting, pickupCode, showToast]); + }, [isWebSocketConnected, isConnected, isConnecting, pickupCode, showToast, fileList.length, currentTransferFile]); // 监听连接状态变化,清理传输状态 useEffect(() => { @@ -646,8 +651,8 @@ export const WebRTCFileTransfer: React.FC = () => { console.log('正在建立WebRTC连接...'); } - // 只有在连接成功且没有错误时才发送文件列表 - if (isConnected && !error && pickupCode && mode === 'send' && selectedFiles.length > 0) { + // 只有在P2P连接建立且没有错误时才发送文件列表 + if (isConnected && connection.isPeerConnected && !error && pickupCode && mode === 'send' && selectedFiles.length > 0) { // 确保有文件列表 if (fileList.length === 0) { console.log('创建文件列表并发送...'); @@ -662,7 +667,7 @@ export const WebRTCFileTransfer: React.FC = () => { setFileList(newFileInfos); // 延迟发送,确保数据通道已准备好 setTimeout(() => { - if (isConnected && !error) { // 再次检查连接状态 + if (isConnected && connection.isPeerConnected && !error) { // 再次检查连接状态 sendFileList(newFileInfos); } }, 500); @@ -670,13 +675,26 @@ export const WebRTCFileTransfer: React.FC = () => { console.log('发送现有文件列表...'); // 延迟发送,确保数据通道已准备好 setTimeout(() => { - if (isConnected && !error) { // 再次检查连接状态 + if (isConnected && connection.isPeerConnected && !error) { // 再次检查连接状态 sendFileList(fileList); } }, 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) => { @@ -765,7 +783,8 @@ export const WebRTCFileTransfer: React.FC = () => { console.log('=== 清空文件 ==='); setSelectedFiles([]); setFileList([]); - if (isConnected && pickupCode) { + // 只有在P2P连接建立且数据通道准备好时才发送清空消息 + if (isConnected && connection.isPeerConnected && connection.getChannelState() === 'open' && pickupCode) { sendFileList([]); } }; diff --git a/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx b/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx index 9d78baa..6f87ad4 100644 --- a/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx +++ b/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx @@ -106,8 +106,7 @@ export const WebRTCTextReceiver: React.FC = ({ setIsTyping(false); // 断开连接 - textTransfer.disconnect(); - fileTransfer.disconnect(); + connection.disconnect(); if (onRestart) { onRestart(); diff --git a/chuan-next/src/components/webrtc/WebRTCTextSender.tsx b/chuan-next/src/components/webrtc/WebRTCTextSender.tsx index d363afa..f96bf00 100644 --- a/chuan-next/src/components/webrtc/WebRTCTextSender.tsx +++ b/chuan-next/src/components/webrtc/WebRTCTextSender.tsx @@ -38,11 +38,9 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o // 连接所有传输通道 const connectAll = useCallback(async (code: string, role: 'sender' | 'receiver') => { console.log('=== 连接所有传输通道 ===', { code, role }); - await Promise.all([ - textTransfer.connect(code, role), - fileTransfer.connect(code, role) - ]); - }, [textTransfer, fileTransfer]); + // 只需要连接一次,因为使用的是共享连接 + await connection.connect(code, role); + }, [connection]); // 是否有任何连接 const hasAnyConnection = textTransfer.isConnected || fileTransfer.isConnected; @@ -63,9 +61,8 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o sentImages.forEach(img => URL.revokeObjectURL(img.url)); setSentImages([]); - // 断开连接 - textTransfer.disconnect(); - fileTransfer.disconnect(); + // 断开连接(只需要断开一次) + connection.disconnect(); if (onRestart) { onRestart(); @@ -141,7 +138,7 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o // 如果有初始文本,发送它 if (currentText) { setTimeout(() => { - if (textTransfer.isConnected) { + if (connection.isPeerConnected && textTransfer.isConnected) { // 发送实时文本同步 textTransfer.sendTextSync(currentText); @@ -171,8 +168,8 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o const newHeight = Math.min(Math.max(textarea.scrollHeight, 100), 300); // 最小100px,最大300px textarea.style.height = `${newHeight}px`; - // 实时同步文本内容(如果已连接) - if (textTransfer.isConnected) { + // 实时同步文本内容(如果P2P连接已建立) + if (connection.isPeerConnected && textTransfer.isConnected) { // 发送实时文本同步 textTransfer.sendTextSync(value); @@ -215,9 +212,11 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o }]); // 发送文件 - if (fileTransfer.isConnected) { + if (connection.isPeerConnected && fileTransfer.isConnected) { fileTransfer.sendFile(file); showToast('图片发送中...', "success"); + } else if (!connection.isPeerConnected) { + showToast('等待对方加入P2P网络...', "error"); } else { showToast('请先连接到房间', "error"); } @@ -409,8 +408,16 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o value={textInput} onChange={handleTextInputChange} onPaste={handlePaste} - placeholder="在这里编辑文字内容... 💡 支持实时同步编辑,对方可以看到你的修改 💡 可以直接粘贴图片 (Ctrl+V)" - 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" + disabled={!connection.isPeerConnected} + 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" + }`} />
@@ -419,7 +426,10 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o onClick={() => fileInputRef.current?.click()} variant="outline" 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' : '' + }`} > 添加图片 diff --git a/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts b/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts new file mode 100644 index 0000000..f59960a --- /dev/null +++ b/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts @@ -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({ + isSharing: false, + isViewing: false, + connectionCode: '', + remoteStream: null, + error: null, + isWaitingForPeer: false, + }); + + const localStreamRef = useRef(null); + const remoteVideoRef = useRef(null); + const currentSenderRef = useRef(null); + + const updateState = useCallback((updates: Partial) => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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, + }; +} diff --git a/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts b/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts index f9efe49..5c0e988 100644 --- a/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts +++ b/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts @@ -3,6 +3,10 @@ import type { WebRTCConnection } from './useSharedWebRTCManager'; // 文件传输状态 interface FileTransferState { + isConnecting: boolean; + isConnected: boolean; + isWebSocketConnected: boolean; + connectionError: string | null; isTransferring: boolean; progress: number; error: string | null; @@ -50,6 +54,10 @@ const CHUNK_SIZE = 256 * 1024; // 256KB export function useFileTransferBusiness(connection: WebRTCConnection) { const [state, setState] = useState({ + isConnecting: false, + isConnected: false, + isWebSocketConnected: false, + connectionError: null, isTransferring: false, progress: 0, error: null, @@ -177,6 +185,17 @@ export function useFileTransferBusiness(connection: WebRTCConnection) { }; }, [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') => { return connection.connect(roomCode, role); @@ -263,6 +282,11 @@ export function useFileTransferBusiness(connection: WebRTCConnection) { // 发送文件列表 const sendFileList = useCallback((fileList: FileInfo[]) => { + if (!connection.isPeerConnected) { + console.log('P2P连接未建立,等待连接后再发送文件列表'); + return; + } + if (connection.getChannelState() !== 'open') { console.error('数据通道未准备就绪,无法发送文件列表'); return; @@ -313,13 +337,7 @@ export function useFileTransferBusiness(connection: WebRTCConnection) { }, []); return { - // 继承基础连接状态 - isConnected: connection.isConnected, - isConnecting: connection.isConnecting, - isWebSocketConnected: connection.isWebSocketConnected, - connectionError: connection.error, - - // 文件传输状态 + // 文件传输状态(包括连接状态) ...state, // 操作方法 diff --git a/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts b/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts index 4d0cb4a..1661d8e 100644 --- a/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts +++ b/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts @@ -1,11 +1,12 @@ import { useState, useRef, useCallback } from 'react'; -import { config } from '@/lib/config'; +import { getWsUrl } from '@/lib/config'; // 基础连接状态 interface WebRTCState { isConnected: boolean; isConnecting: boolean; isWebSocketConnected: boolean; + isPeerConnected: boolean; // 新增:P2P连接状态 error: string | null; } @@ -26,6 +27,7 @@ export interface WebRTCConnection { isConnected: boolean; isConnecting: boolean; isWebSocketConnected: boolean; + isPeerConnected: boolean; // 新增:P2P连接状态 error: string | null; // 操作方法 @@ -44,6 +46,13 @@ export interface WebRTCConnection { // 当前房间信息 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; } /** @@ -55,6 +64,7 @@ export function useSharedWebRTCManager(): WebRTCConnection { isConnected: false, isConnecting: false, isWebSocketConnected: false, + isPeerConnected: false, error: null, }); @@ -70,12 +80,12 @@ export function useSharedWebRTCManager(): WebRTCConnection { const messageHandlers = useRef>(new Map()); const dataHandlers = useRef>(new Map()); - // STUN 服务器配置 + // STUN 服务器配置 - 使用更稳定的服务器 const STUN_SERVERS = [ - { urls: 'stun:stun.chat.bilibili.com' }, { urls: 'stun:stun.l.google.com:19302' }, - { urls: 'stun:stun.miwifi.com' }, - { urls: 'stun:turn.cloudflare.com:3478' }, + { urls: 'stun:stun1.l.google.com:19302' }, + { urls: 'stun:stun2.l.google.com:19302' }, + { urls: 'stun:global.stun.twilio.com:3478' }, ]; const updateState = useCallback((updates: Partial) => { @@ -84,44 +94,47 @@ export function useSharedWebRTCManager(): WebRTCConnection { // 清理连接 const cleanup = useCallback(() => { - // console.log('[SharedWebRTC] 清理连接'); - // if (timeoutRef.current) { - // clearTimeout(timeoutRef.current); - // timeoutRef.current = null; - // } + console.log('[SharedWebRTC] 清理连接'); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } - // if (dcRef.current) { - // dcRef.current.close(); - // dcRef.current = null; - // } + if (dcRef.current) { + dcRef.current.close(); + dcRef.current = null; + } - // if (pcRef.current) { - // pcRef.current.close(); - // pcRef.current = null; - // } + if (pcRef.current) { + pcRef.current.close(); + pcRef.current = null; + } - // if (wsRef.current) { - // wsRef.current.close(); - // wsRef.current = null; - // } + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } - // currentRoom.current = null; + currentRoom.current = null; }, []); // 创建 Offer const createOffer = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => { try { + console.log('[SharedWebRTC] 🎬 开始创建offer,当前轨道数量:', pc.getSenders().length); + const offer = await pc.createOffer({ - offerToReceiveAudio: false, - offerToReceiveVideo: false, + offerToReceiveAudio: true, // 改为true以支持音频接收 + offerToReceiveVideo: true, // 改为true以支持视频接收 }); + console.log('[SharedWebRTC] 📝 Offer创建成功,设置本地描述...'); await pc.setLocalDescription(offer); const iceTimeout = setTimeout(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription })); - console.log('[SharedWebRTC] 发送 offer (超时发送)'); + console.log('[SharedWebRTC] 📤 发送 offer (超时发送)'); } }, 3000); @@ -129,7 +142,7 @@ export function useSharedWebRTCManager(): WebRTCConnection { clearTimeout(iceTimeout); if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription })); - console.log('[SharedWebRTC] 发送 offer (ICE收集完成)'); + console.log('[SharedWebRTC] 📤 发送 offer (ICE收集完成)'); } } else { pc.onicegatheringstatechange = () => { @@ -137,13 +150,13 @@ export function useSharedWebRTCManager(): WebRTCConnection { clearTimeout(iceTimeout); if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription })); - console.log('[SharedWebRTC] 发送 offer (ICE收集完成)'); + console.log('[SharedWebRTC] 📤 发送 offer (ICE收集完成)'); } } }; } } catch (error) { - console.error('[SharedWebRTC] 创建 offer 失败:', error); + console.error('[SharedWebRTC] ❌ 创建 offer 失败:', error); updateState({ error: '创建连接失败', isConnecting: false }); } }, [updateState]); @@ -187,60 +200,24 @@ export function useSharedWebRTCManager(): WebRTCConnection { // 连接到房间 const connect = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => { - 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((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, - }); - } + console.log('[SharedWebRTC] 🚀 开始连接到房间:', roomCode, role); + // 如果正在连接中,避免重复连接 if (state.isConnecting) { - console.warn('[SharedWebRTC] 正在连接中,跳过重复连接请求'); + console.warn('[SharedWebRTC] ⚠️ 正在连接中,跳过重复连接请求'); return; } + // 清理之前的连接 cleanup(); currentRoom.current = { code: roomCode, role }; updateState({ isConnecting: true, error: null }); - // 设置连接超时 - timeoutRef.current = setTimeout(() => { - console.warn('[SharedWebRTC] 连接超时'); - updateState({ error: '连接超时,请检查网络状况或重新尝试', isConnecting: false }); - cleanup(); - }, 30000); + // 注意:不在这里设置超时,因为WebSocket连接很快, + // WebRTC连接的建立是在后续添加轨道时进行的 try { + console.log('[SharedWebRTC] 🔧 创建PeerConnection...'); // 创建 PeerConnection const pc = new RTCPeerConnection({ iceServers: STUN_SERVERS, @@ -248,69 +225,119 @@ export function useSharedWebRTCManager(): WebRTCConnection { }); pcRef.current = pc; - // 连接 WebSocket - const wsUrl = config.api.wsUrl.replace('/ws/p2p', '/ws/webrtc'); - const ws = new WebSocket(`${wsUrl}?code=${roomCode}&role=${role}&channel=shared`); + // 连接 WebSocket - 使用动态URL + const baseWsUrl = getWsUrl(); + 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; // WebSocket 事件处理 ws.onopen = () => { - console.log('[SharedWebRTC] WebSocket 连接已建立'); - updateState({ isWebSocketConnected: true }); - - if (role === 'sender') { - createOffer(pc, ws); - } + console.log('[SharedWebRTC] ✅ WebSocket 连接已建立,房间准备就绪'); + updateState({ + isWebSocketConnected: true, + isConnecting: false, // WebSocket连接成功即表示初始连接完成 + isConnected: true // 可以开始后续操作 + }); }; ws.onmessage = async (event) => { try { const message = JSON.parse(event.data); - console.log('[SharedWebRTC] 收到信令消息:', message.type); + console.log('[SharedWebRTC] 📨 收到信令消息:', 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': + console.log('[SharedWebRTC] 📬 处理offer...'); if (pc.signalingState === 'stable') { await pc.setRemoteDescription(new RTCSessionDescription(message.payload)); + console.log('[SharedWebRTC] ✅ 设置远程描述完成'); + const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); + console.log('[SharedWebRTC] ✅ 创建并设置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; case 'answer': + console.log('[SharedWebRTC] 📬 处理answer...'); if (pc.signalingState === 'have-local-offer') { 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; case 'ice-candidate': if (message.payload && pc.remoteDescription) { - await pc.addIceCandidate(new RTCIceCandidate(message.payload)); - console.log('[SharedWebRTC] 添加 ICE 候选'); + try { + await pc.addIceCandidate(new RTCIceCandidate(message.payload)); + console.log('[SharedWebRTC] ✅ 添加 ICE 候选成功'); + } catch (err) { + console.warn('[SharedWebRTC] ⚠️ 添加 ICE 候选失败:', err); + } + } else { + console.warn('[SharedWebRTC] ⚠️ ICE候选无效或远程描述未设置'); } break; case 'error': - console.error('[SharedWebRTC] 信令错误:', message.error); + console.error('[SharedWebRTC] ❌ 信令服务器错误:', message.error); updateState({ error: message.error, isConnecting: false }); break; + + default: + console.warn('[SharedWebRTC] ⚠️ 未知消息类型:', message.type); } } catch (error) { - console.error('[SharedWebRTC] 处理信令消息失败:', error); + console.error('[SharedWebRTC] ❌ 处理信令消息失败:', error); + updateState({ error: '信令处理失败: ' + error, isConnecting: false }); } }; ws.onerror = (error) => { - console.error('[SharedWebRTC] WebSocket 错误:', error); - updateState({ error: 'WebSocket连接失败,请检查网络连接', isConnecting: false }); + console.error('[SharedWebRTC] ❌ WebSocket 错误:', error); + updateState({ error: 'WebSocket连接失败,请检查服务器是否运行在8080端口', isConnecting: false }); }; - ws.onclose = () => { - console.log('[SharedWebRTC] WebSocket 连接已关闭'); + ws.onclose = (event) => { + console.log('[SharedWebRTC] 🔌 WebSocket 连接已关闭, 代码:', event.code, '原因:', event.reason); updateState({ isWebSocketConnected: false }); + if (event.code !== 1000 && event.code !== 1001) { // 非正常关闭 + updateState({ error: `WebSocket异常关闭 (${event.code}): ${event.reason || '未知原因'}`, isConnecting: false }); + } }; // PeerConnection 事件处理 @@ -320,32 +347,63 @@ export function useSharedWebRTCManager(): WebRTCConnection { type: 'ice-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 = () => { - console.log('[SharedWebRTC] 连接状态变化:', pc.connectionState); + console.log('[SharedWebRTC] 🔗 WebRTC连接状态变化:', pc.connectionState); switch (pc.connectionState) { + case 'connecting': + console.log('[SharedWebRTC] 🔄 WebRTC正在连接中...'); + updateState({ isPeerConnected: false }); + break; case 'connected': - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - updateState({ isConnected: true, isConnecting: false, error: null }); + console.log('[SharedWebRTC] 🎉 WebRTC P2P连接已完全建立,可以进行媒体传输'); + updateState({ isPeerConnected: true, error: null }); break; case 'failed': - updateState({ error: 'WebRTC连接失败,可能是网络防火墙阻止了连接', isConnecting: false, isConnected: false }); - break; - case 'disconnected': - updateState({ isConnected: false }); - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; + // 只有在数据通道也未打开的情况下才认为连接真正失败 + const currentDc = dcRef.current; + if (!currentDc || currentDc.readyState !== 'open') { + console.error('[SharedWebRTC] ❌ WebRTC连接失败,数据通道未建立'); + updateState({ error: 'WebRTC连接失败,请检查网络设置或重试', isPeerConnected: false }); + } else { + console.log('[SharedWebRTC] ⚠️ WebRTC连接状态为failed,但数据通道正常,忽略此状态'); } break; + case 'disconnected': + console.log('[SharedWebRTC] 🔌 WebRTC连接已断开'); + updateState({ isPeerConnected: false }); + break; case 'closed': - updateState({ isConnected: false, isConnecting: false }); + console.log('[SharedWebRTC] 🚫 WebRTC连接已关闭'); + updateState({ isPeerConnected: false }); break; } }; @@ -360,6 +418,7 @@ export function useSharedWebRTCManager(): WebRTCConnection { dataChannel.onopen = () => { console.log('[SharedWebRTC] 数据通道已打开 (发送方)'); + updateState({ isPeerConnected: true, error: null, isConnecting: false }); }; dataChannel.onmessage = handleDataChannelMessage; @@ -375,6 +434,7 @@ export function useSharedWebRTCManager(): WebRTCConnection { dataChannel.onopen = () => { console.log('[SharedWebRTC] 数据通道已打开 (接收方)'); + updateState({ isPeerConnected: true, error: null, isConnecting: false }); }; 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) { console.error('[SharedWebRTC] 连接失败:', error); updateState({ @@ -403,6 +476,7 @@ export function useSharedWebRTCManager(): WebRTCConnection { isConnected: false, isConnecting: false, isWebSocketConnected: false, + isPeerConnected: false, error: null, }); }, [cleanup]); @@ -478,11 +552,90 @@ export function useSharedWebRTCManager(): WebRTCConnection { 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 { // 状态 isConnected: state.isConnected, isConnecting: state.isConnecting, isWebSocketConnected: state.isWebSocketConnected, + isPeerConnected: state.isPeerConnected, error: state.error, // 操作方法 @@ -499,6 +652,13 @@ export function useSharedWebRTCManager(): WebRTCConnection { getChannelState, isConnectedToRoom, + // 媒体轨道方法 + addTrack, + removeTrack, + onTrack, + getPeerConnection, + createOfferNow, + // 当前房间信息 currentRoom: currentRoom.current, }; diff --git a/chuan-next/src/hooks/webrtc/useTextTransferBusiness.ts b/chuan-next/src/hooks/webrtc/useTextTransferBusiness.ts index 1cc845e..39682bd 100644 --- a/chuan-next/src/hooks/webrtc/useTextTransferBusiness.ts +++ b/chuan-next/src/hooks/webrtc/useTextTransferBusiness.ts @@ -84,9 +84,14 @@ export function useTextTransferBusiness(connection: WebRTCConnection) { // 监听连接状态变化 (直接使用 connection 的状态) 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') => { @@ -100,28 +105,32 @@ export function useTextTransferBusiness(connection: WebRTCConnection) { // 发送实时文本同步 (替代原来的 sendMessage) const sendTextSync = useCallback((text: string) => { - if (!connection) return; + if (!connection || !connection.isPeerConnected) return; const message = { type: 'text-sync', payload: { text } }; - connection.sendMessage(message, CHANNEL_NAME); - console.log('发送实时文本同步:', text.length, '字符'); + const success = connection.sendMessage(message, CHANNEL_NAME); + if (success) { + console.log('发送实时文本同步:', text.length, '字符'); + } }, [connection]); // 发送打字状态 const sendTypingStatus = useCallback((isTyping: boolean) => { - if (!connection) return; + if (!connection || !connection.isPeerConnected) return; const message = { type: 'text-typing', payload: { typing: isTyping } }; - connection.sendMessage(message, CHANNEL_NAME); - console.log('发送打字状态:', isTyping); + const success = connection.sendMessage(message, CHANNEL_NAME); + if (success) { + console.log('发送打字状态:', isTyping); + } }, [connection]); // 设置文本同步回调 diff --git a/chuan-next/src/lib/config.ts b/chuan-next/src/lib/config.ts index 476c216..2c743fc 100644 --- a/chuan-next/src/lib/config.ts +++ b/chuan-next/src/lib/config.ts @@ -24,7 +24,7 @@ const getCurrentBaseUrl = () => { return 'http://localhost:8080'; }; -// 动态获取 WebSocket URL +// 动态获取 WebSocket URL - 总是在客户端运行时计算 const getCurrentWsUrl = () => { if (typeof window !== 'undefined') { // 检查是否是 Next.js 开发服务器(端口 3000 或 3001) @@ -40,8 +40,8 @@ const getCurrentWsUrl = () => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}/ws/p2p`; } - // 服务器端默认值 - return 'ws://localhost:8080/ws/p2p'; + // 服务器端返回空字符串,强制在客户端计算 + return ''; }; export const config = { @@ -61,8 +61,8 @@ export const config = { // 直接后端URL (客户端在静态模式下使用) - 如果环境变量为空,则使用当前域名 directBackendUrl: getEnv('NEXT_PUBLIC_BACKEND_URL') || getCurrentBaseUrl(), - // WebSocket地址 - 如果环境变量为空,则使用当前域名构建 - wsUrl: getEnv('NEXT_PUBLIC_WS_URL') || getCurrentWsUrl(), + // WebSocket地址 - 在客户端运行时动态计算,不在构建时预设 + wsUrl: '', // 将通过 getWsUrl() 函数动态获取 }, // 超时配置 @@ -113,12 +113,23 @@ export function getDirectBackendUrl(path: string): string { } /** - * 获取WebSocket URL + * 获取WebSocket URL - 总是在客户端运行时动态计算 * @returns WebSocket连接地址 */ 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(); } /** diff --git a/chuan-next/src/styles/animations.css b/chuan-next/src/styles/animations.css new file mode 100644 index 0000000..cef8be7 --- /dev/null +++ b/chuan-next/src/styles/animations.css @@ -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; +} diff --git a/internal/services/webrtc_service.go b/internal/services/webrtc_service.go index c11168c..aae326c 100644 --- a/internal/services/webrtc_service.go +++ b/internal/services/webrtc_service.go @@ -138,8 +138,33 @@ func (ws *WebRTCService) addClientToRoom(code string, client *WebRTCClient) { if client.Role == "sender" { 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 { 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,立即发送给接收方 if room.LastOffer != nil { log.Printf("向新连接的接收方发送保存的offer")