diff --git a/chuan-next/src/components/DesktopShare.tsx b/chuan-next/src/components/DesktopShare.tsx index 879bb5e..dd9f815 100644 --- a/chuan-next/src/components/DesktopShare.tsx +++ b/chuan-next/src/components/DesktopShare.tsx @@ -3,15 +3,13 @@ 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, 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'; +import { Share, Monitor } from 'lucide-react'; +import WebRTCDesktopReceiver from '@/components/webrtc/WebRTCDesktopReceiver'; +import WebRTCDesktopSender from '@/components/webrtc/WebRTCDesktopSender'; + interface DesktopShareProps { - // 保留向后兼容性的props + // 保留向后兼容性的props(已废弃,但保留接口) onStartSharing?: () => Promise; onStopSharing?: () => Promise; onJoinSharing?: (code: string) => Promise; @@ -25,24 +23,19 @@ export default function DesktopShare({ const searchParams = useSearchParams(); const router = useRouter(); const [mode, setMode] = useState<'share' | 'view'>('share'); - const [inputCode, setInputCode] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [showDebug, setShowDebug] = useState(false); - const { showToast } = useToast(); - // 使用桌面共享业务逻辑 - const desktopShare = useDesktopShareBusiness(); - - // 从URL参数中获取初始模式 + // 从URL参数中获取初始模式和房间代码 useEffect(() => { const urlMode = searchParams.get('mode'); const type = searchParams.get('type'); + const urlCode = searchParams.get('code'); if (type === 'desktop' && urlMode) { if (urlMode === 'send') { setMode('share'); } else if (urlMode === 'receive') { setMode('view'); + // 如果URL中有房间代码,将在DesktopShareReceiver组件中自动加入 } } }, [searchParams]); @@ -53,144 +46,26 @@ export default function DesktopShare({ const currentUrl = new URL(window.location.href); currentUrl.searchParams.set('type', 'desktop'); currentUrl.searchParams.set('mode', newMode === 'share' ? 'send' : 'receive'); + // 清除代码参数,避免模式切换时的混乱 + currentUrl.searchParams.delete('code'); router.replace(currentUrl.pathname + currentUrl.search); }, [router]); - // 复制房间代码 - const copyCode = useCallback(async (code: string) => { - try { - await navigator.clipboard.writeText(code); - showToast('房间代码已复制到剪贴板', 'success'); - } catch (error) { - console.error('复制失败:', error); - showToast('复制失败,请手动复制', 'error'); + // 获取初始房间代码(用于接收者模式) + const getInitialCode = useCallback(() => { + const urlMode = searchParams.get('mode'); + const type = searchParams.get('type'); + const code = searchParams.get('code'); + console.log('[DesktopShare] getInitialCode 调用, URL参数:', { type, urlMode, code }); + + if (type === 'desktop' && urlMode === 'receive') { + const result = code || ''; + console.log('[DesktopShare] getInitialCode 返回:', result); + return result; } - }, [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(); + console.log('[DesktopShare] getInitialCode 返回空字符串'); + return ''; + }, [searchParams]); return (
@@ -216,425 +91,14 @@ export default function DesktopShare({
+ {/* 根据模式渲染对应的组件 */} {mode === 'share' ? ( - /* 共享模式 */ -
- {!desktopShare.connectionCode ? ( - // 创建房间前的界面 -
- {/* 功能标题和状态 */} -
-
-
- -
-
-

共享桌面

-

分享您的屏幕给其他人

-
-
- - {/* 竖线分割 */} -
- - {/* 状态显示 */} -
-
连接状态
-
- {/* WebSocket状态 */} -
-
- WS -
- - {/* 分隔符 */} -
|
- - {/* WebRTC状态 */} -
-
- RTC -
-
-
-
- -
-
- -
-

创建桌面共享房间

-

创建房间后将生成分享码,等待接收方加入后即可开始桌面共享

- - -
-
- ) : ( - // 房间已创建,显示取件码和等待界面 -
- {/* 功能标题和状态 */} -
-
-
- -
-
-

共享桌面

-

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

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

- - 桌面共享控制 -

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

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

-
-
- 正在等待连接 -
-
- )} -
- ) : ( -
-
- - 桌面共享进行中 -
-
- - -
-
- )} -
-
- )} - - {/* 取件码显示 - 和文件传输一致的风格 */} -
- {/* 左上角状态提示 */} -
-
-
- -
-
-

房间码生成成功!

-

分享以下信息给观看方

-
-
-
- - {/* 中间区域:取件码 + 分隔线 + 二维码 */} -
- {/* 左侧:取件码 */} -
- -
-
- {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}

-
- - {/* 观看中的控制面板 */} -
-
-
- - 观看中 -
- -
-
- - {/* 桌面显示区域 */} - {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 index 867845c..04f865c 100644 --- a/chuan-next/src/components/DesktopViewer.tsx +++ b/chuan-next/src/components/DesktopViewer.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useRef, useEffect, useState, useCallback } from 'react'; -import { Monitor, Maximize, Minimize, Volume2, VolumeX, Settings, X } from 'lucide-react'; +import { Monitor, Maximize, Minimize, Volume2, VolumeX, Settings, X, Play } from 'lucide-react'; import { Button } from '@/components/ui/button'; interface DesktopViewerProps { @@ -22,6 +22,9 @@ export default function DesktopViewer({ const [isFullscreen, setIsFullscreen] = useState(false); const [isMuted, setIsMuted] = useState(false); const [showControls, setShowControls] = useState(true); + const [isPlaying, setIsPlaying] = useState(false); + const [needsUserInteraction, setNeedsUserInteraction] = useState(false); + const hasAttemptedAutoplayRef = useRef(false); const [videoStats, setVideoStats] = useState<{ resolution: string; fps: number; @@ -39,9 +42,69 @@ export default function DesktopViewer({ videoRef.current.srcObject = stream; console.log('[DesktopViewer] ✅ 视频元素已设置流'); + + // 重置状态 + hasAttemptedAutoplayRef.current = false; + setNeedsUserInteraction(false); + setIsPlaying(false); + + // 添加事件监听器来调试视频加载 + const video = videoRef.current; + const handleLoadStart = () => console.log('[DesktopViewer] 📹 视频开始加载'); + const handleLoadedMetadata = () => { + console.log('[DesktopViewer] 📹 视频元数据已加载'); + console.log('[DesktopViewer] 📹 视频尺寸:', video.videoWidth, 'x', video.videoHeight); + }; + const handleCanPlay = () => { + console.log('[DesktopViewer] 📹 视频可以开始播放'); + // 只在还未尝试过自动播放时才尝试 + if (!hasAttemptedAutoplayRef.current) { + hasAttemptedAutoplayRef.current = true; + video.play() + .then(() => { + console.log('[DesktopViewer] ✅ 视频自动播放成功'); + setIsPlaying(true); + setNeedsUserInteraction(false); + }) + .catch(e => { + console.log('[DesktopViewer] 📹 自动播放被阻止,需要用户交互:', e.message); + setIsPlaying(false); + setNeedsUserInteraction(true); + }); + } + }; + const handlePlay = () => { + console.log('[DesktopViewer] 📹 视频开始播放'); + setIsPlaying(true); + setNeedsUserInteraction(false); + }; + const handlePause = () => { + console.log('[DesktopViewer] 📹 视频暂停'); + setIsPlaying(false); + }; + const handleError = (e: Event) => console.error('[DesktopViewer] 📹 视频播放错误:', e); + + video.addEventListener('loadstart', handleLoadStart); + video.addEventListener('loadedmetadata', handleLoadedMetadata); + video.addEventListener('canplay', handleCanPlay); + video.addEventListener('play', handlePlay); + video.addEventListener('pause', handlePause); + video.addEventListener('error', handleError); + + return () => { + video.removeEventListener('loadstart', handleLoadStart); + video.removeEventListener('loadedmetadata', handleLoadedMetadata); + video.removeEventListener('canplay', handleCanPlay); + video.removeEventListener('play', handlePlay); + video.removeEventListener('pause', handlePause); + video.removeEventListener('error', handleError); + }; } else if (videoRef.current && !stream) { console.log('[DesktopViewer] ❌ 清除视频流'); videoRef.current.srcObject = null; + setIsPlaying(false); + setNeedsUserInteraction(false); + hasAttemptedAutoplayRef.current = false; } }, [stream]); @@ -176,6 +239,21 @@ export default function DesktopViewer({ } }, []); + // 手动播放视频 + const handleManualPlay = useCallback(() => { + if (videoRef.current) { + videoRef.current.play() + .then(() => { + console.log('[DesktopViewer] ✅ 手动播放成功'); + setIsPlaying(true); + setNeedsUserInteraction(false); + }) + .catch(e => { + console.error('[DesktopViewer] ❌ 手动播放失败:', e); + }); + } + }, []); + // 清理定时器 useEffect(() => { return () => { @@ -223,6 +301,19 @@ export default function DesktopViewer({ }} /> + {/* 需要用户交互的播放覆盖层 - 只在自动播放尝试失败后显示 */} + {hasAttemptedAutoplayRef.current && needsUserInteraction && !isPlaying && ( +
+
+
+ +
+

点击播放桌面共享

+

浏览器需要用户交互才能开始播放媒体

+
+
+ )} + {/* 连接状态覆盖层 */} {!isConnected && (
@@ -244,8 +335,8 @@ export default function DesktopViewer({ {/* 左侧信息 */}
-
- 桌面共享中 +
+ {isPlaying ? '桌面共享中' : needsUserInteraction ? '等待播放' : '连接中'}
{videoStats.resolution !== '0x0' && ( <> diff --git a/chuan-next/src/components/RoomInfoDisplay.tsx b/chuan-next/src/components/RoomInfoDisplay.tsx new file mode 100644 index 0000000..f17aa29 --- /dev/null +++ b/chuan-next/src/components/RoomInfoDisplay.tsx @@ -0,0 +1,123 @@ +"use client"; + +import React from 'react'; +import { Button } from '@/components/ui/button'; +import QRCodeDisplay from '@/components/QRCodeDisplay'; +import { LucideIcon } from 'lucide-react'; + +interface RoomInfoDisplayProps { + // 房间信息 + code: string; + link: string; + + // 显示配置 + icon: LucideIcon; + iconColor?: string; // 图标背景渐变色,如 'from-emerald-500 to-teal-500' + codeColor?: string; // 代码文字渐变色,如 'from-emerald-600 to-teal-600' + + // 文案配置 + title: string; // 如 "取件码生成成功!" 或 "房间码生成成功!" + subtitle: string; // 如 "分享以下信息给接收方" 或 "分享以下信息给观看方" + codeLabel: string; // 如 "取件码" 或 "房间代码" + qrLabel: string; // 如 "扫码传输" 或 "扫码观看" + copyButtonText: string; // 如 "复制取件码" 或 "复制房间代码" + copyButtonColor?: string; // 复制按钮颜色,如 'bg-emerald-500 hover:bg-emerald-600' + qrButtonText: string; // 如 "使用手机扫码快速访问" 或 "使用手机扫码快速观看" + linkButtonText: string; // 如 "复制取件链接" 或 "复制观看链接" + + // 事件回调 + onCopyCode: () => void; + onCopyLink: () => void; + + // 样式配置 + className?: string; +} + +export default function RoomInfoDisplay({ + code, + link, + icon: Icon, + iconColor = 'from-emerald-500 to-teal-500', + codeColor = 'from-emerald-600 to-teal-600', + title, + subtitle, + codeLabel, + qrLabel, + copyButtonText, + copyButtonColor = 'bg-emerald-500 hover:bg-emerald-600', + qrButtonText, + linkButtonText, + onCopyCode, + onCopyLink, + className = '' +}: RoomInfoDisplayProps) { + return ( +
+ {/* 左上角状态提示 */} +
+
+
+ +
+
+

{title}

+

{subtitle}

+
+
+
+ + {/* 中间区域:代码 + 分隔线 + 二维码 */} +
+ {/* 左侧:代码 */} +
+ +
+
+ {code} +
+
+ +
+ + {/* 分隔线 - 大屏幕显示竖线,移动端隐藏 */} +
+ + {/* 右侧:二维码 */} +
+ +
+ +
+
+ {qrButtonText} +
+
+
+ + {/* 底部:链接 */} +
+
+
+
+ {link} +
+
+ +
+
+
+ ); +} diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx new file mode 100644 index 0000000..42620cd --- /dev/null +++ b/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx @@ -0,0 +1,274 @@ +"use client"; + +import React, { useState, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Monitor, Square } from 'lucide-react'; +import { useToast } from '@/components/ui/toast-simple'; +import { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness'; +import DesktopViewer from '@/components/DesktopViewer'; + +interface WebRTCDesktopReceiverProps { + className?: string; + initialCode?: string; // 支持从URL参数传入的房间代码 +} + +export default function WebRTCDesktopReceiver({ className, initialCode }: WebRTCDesktopReceiverProps) { + const [inputCode, setInputCode] = useState(initialCode || ''); + const [isLoading, setIsLoading] = useState(false); + const [showDebug, setShowDebug] = useState(false); + const hasTriedAutoJoin = React.useRef(false); // 添加 ref 来跟踪是否已尝试自动加入 + const { showToast } = useToast(); + + // 使用桌面共享业务逻辑 + const desktopShare = useDesktopShareBusiness(); + + // 加入观看 + const handleJoinViewing = useCallback(async () => { + if (!inputCode.trim()) { + showToast('请输入房间代码', 'error'); + return; + } + + try { + setIsLoading(true); + console.log('[DesktopShareReceiver] 用户加入观看房间:', inputCode); + + await desktopShare.joinSharing(inputCode.trim().toUpperCase()); + console.log('[DesktopShareReceiver] 加入观看成功'); + + showToast('已加入桌面共享', 'success'); + } catch (error) { + console.error('[DesktopShareReceiver] 加入观看失败:', 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('[DesktopShareReceiver] 停止观看失败:', error); + showToast('退出失败', 'error'); + } finally { + setIsLoading(false); + } + }, [desktopShare, showToast]); + + // 如果有初始代码且还未加入观看,自动尝试加入 + React.useEffect(() => { + console.log('[WebRTCDesktopReceiver] useEffect 触发, 参数:', { + initialCode, + isViewing: desktopShare.isViewing, + isConnecting: desktopShare.isConnecting, + hasTriedAutoJoin: hasTriedAutoJoin.current + }); + + const autoJoin = async () => { + if (initialCode && !desktopShare.isViewing && !desktopShare.isConnecting && !hasTriedAutoJoin.current) { + hasTriedAutoJoin.current = true; + console.log('[WebRTCDesktopReceiver] 检测到初始代码,自动加入观看:', initialCode); + + try { + setIsLoading(true); + await desktopShare.joinSharing(initialCode.trim().toUpperCase()); + console.log('[WebRTCDesktopReceiver] 自动加入观看成功'); + showToast('已加入桌面共享', 'success'); + } catch (error) { + console.error('[WebRTCDesktopReceiver] 自动加入观看失败:', error); + const errorMessage = error instanceof Error ? error.message : '加入观看失败'; + showToast(errorMessage, 'error'); + } finally { + setIsLoading(false); + } + } else { + console.log('[WebRTCDesktopReceiver] 不满足自动加入条件:', { + hasInitialCode: !!initialCode, + notViewing: !desktopShare.isViewing, + notConnecting: !desktopShare.isConnecting, + notTriedBefore: !hasTriedAutoJoin.current + }); + } + }; + + autoJoin(); + }, [initialCode, desktopShare.isViewing, desktopShare.isConnecting]); // 移除了 desktopShare.joinSharing 和 showToast + + return ( +
+
+
+ {!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}

+
+ + {/* 观看中的控制面板 */} +
+
+
+ + 观看中 +
+ +
+
+ + {/* 桌面显示区域 */} + {desktopShare.remoteStream ? ( + + ) : ( +
+
+ +

等待接收桌面画面...

+

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

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

{desktopShare.error}

+
+ )} + + {/* 调试信息 */} +
+ + + {showDebug && ( +
+
WebSocket连接: {desktopShare.isWebSocketConnected ? '✅' : '❌'}
+
P2P连接: {desktopShare.isPeerConnected ? '✅' : '❌'}
+
观看状态: {desktopShare.isViewing ? '观看中' : '未观看'}
+
远程流: {desktopShare.remoteStream ? '已接收' : '无'}
+ {desktopShare.remoteStream && ( +
+
流轨道数量: {desktopShare.remoteStream.getTracks().length}
+
视频轨道: {desktopShare.remoteStream.getVideoTracks().length}
+
音频轨道: {desktopShare.remoteStream.getAudioTracks().length}
+ {desktopShare.remoteStream.getVideoTracks().map((track, index) => ( +
+ 视频轨道{index}: {track.readyState}, enabled: {track.enabled ? '是' : '否'} +
+ ))} + {desktopShare.remoteStream.getAudioTracks().map((track, index) => ( +
+ 音频轨道{index}: {track.readyState}, enabled: {track.enabled ? '是' : '否'} +
+ ))} +
+ )} +
+ )} +
+
+ ); +} diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx new file mode 100644 index 0000000..dedbfc6 --- /dev/null +++ b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx @@ -0,0 +1,353 @@ +"use client"; + +import React, { useState, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +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 QRCodeDisplay from '@/components/QRCodeDisplay'; +import RoomInfoDisplay from '@/components/RoomInfoDisplay'; + +interface WebRTCDesktopSenderProps { + className?: string; +} + +export default function WebRTCDesktopSender({ className }: WebRTCDesktopSenderProps) { + const [isLoading, setIsLoading] = useState(false); + const [showDebug, setShowDebug] = useState(false); + const { showToast } = useToast(); + + // 使用桌面共享业务逻辑 + const desktopShare = useDesktopShareBusiness(); + + // 复制房间代码 + const copyCode = useCallback(async (code: string) => { + try { + await navigator.clipboard.writeText(code); + showToast('房间代码已复制到剪贴板', 'success'); + } catch (error) { + console.error('复制失败:', error); + showToast('复制失败,请手动复制', 'error'); + } + }, [showToast]); + + // 创建房间 + const handleCreateRoom = useCallback(async () => { + try { + setIsLoading(true); + console.log('[DesktopShareSender] 用户点击创建房间'); + + const roomCode = await desktopShare.createRoom(); + console.log('[DesktopShareSender] 房间创建成功:', roomCode); + + showToast(`房间创建成功!代码: ${roomCode}`, 'success'); + } catch (error) { + console.error('[DesktopShareSender] 创建房间失败:', 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('[DesktopShareSender] 用户点击开始桌面共享'); + + await desktopShare.startSharing(); + console.log('[DesktopShareSender] 桌面共享开始成功'); + + showToast('桌面共享已开始', 'success'); + } catch (error) { + console.error('[DesktopShareSender] 开始桌面共享失败:', 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('[DesktopShareSender] 用户点击切换桌面'); + + await desktopShare.switchDesktop(); + console.log('[DesktopShareSender] 桌面切换成功'); + + showToast('桌面切换成功', 'success'); + } catch (error) { + console.error('[DesktopShareSender] 切换桌面失败:', 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('[DesktopShareSender] 用户点击停止桌面共享'); + + await desktopShare.stopSharing(); + console.log('[DesktopShareSender] 桌面共享停止成功'); + + showToast('桌面共享已停止', 'success'); + } catch (error) { + console.error('[DesktopShareSender] 停止桌面共享失败:', error); + const errorMessage = error instanceof Error ? error.message : '停止桌面共享失败'; + showToast(errorMessage, 'error'); + } finally { + setIsLoading(false); + } + }, [desktopShare, showToast]); + + return ( +
+
+ {!desktopShare.connectionCode ? ( + // 创建房间前的界面 +
+ {/* 功能标题和状态 */} +
+
+
+ +
+
+

共享桌面

+

分享您的屏幕给其他人

+
+
+ + {/* 竖线分割 */} +
+ + {/* 状态显示 */} +
+
连接状态
+
+ {/* WebSocket状态 */} +
+
+ WS +
+ + {/* 分隔符 */} +
|
+ + {/* WebRTC状态 */} +
+
+ RTC +
+
+
+
+ +
+
+ +
+

创建桌面共享房间

+

创建房间后将生成分享码,等待接收方加入后即可开始桌面共享

+ + +
+
+ ) : ( + // 房间已创建,显示取件码和等待界面 +
+ {/* 功能标题和状态 */} +
+
+
+ +
+
+

共享桌面

+

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

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

+ + 桌面共享控制 +

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

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

+
+
+ 正在等待连接 +
+
+ )} +
+ ) : ( +
+
+ + 桌面共享进行中 +
+
+ + +
+
+ )} +
+
+ )} + + {/* 房间信息显示 */} + copyCode(desktopShare.connectionCode)} + onCopyLink={() => { + const link = `${window.location.origin}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`; + navigator.clipboard.writeText(link); + showToast('观看链接已复制', 'success'); + }} + /> +
+ )} +
+ + {/* 错误显示 */} + {desktopShare.error && ( +
+

{desktopShare.error}

+
+ )} + + {/* 调试信息 */} +
+ + + {showDebug && ( +
+
WebSocket连接: {desktopShare.isWebSocketConnected ? '✅' : '❌'}
+
P2P连接: {desktopShare.isPeerConnected ? '✅' : '❌'}
+
房间代码: {desktopShare.connectionCode || '未创建'}
+
共享状态: {desktopShare.isSharing ? '进行中' : '未共享'}
+
等待对方: {desktopShare.isWaitingForPeer ? '是' : '否'}
+
+ )} +
+
+ ); +} diff --git a/chuan-next/src/components/webrtc/WebRTCFileUpload.tsx b/chuan-next/src/components/webrtc/WebRTCFileUpload.tsx index 3e37f07..85ac9a1 100644 --- a/chuan-next/src/components/webrtc/WebRTCFileUpload.tsx +++ b/chuan-next/src/components/webrtc/WebRTCFileUpload.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/toast-simple'; import { Upload, FileText, Image, Video, Music, Archive, X } from 'lucide-react'; import QRCodeDisplay from '@/components/QRCodeDisplay'; +import RoomInfoDisplay from '@/components/RoomInfoDisplay'; interface FileInfo { id: string; @@ -397,80 +398,24 @@ export function WebRTCFileUpload({
{/* 取件码展示 */} - {pickupCode && ( -
- {/* 左上角状态提示 - 类似已选择文件的风格 */} -
-
-
- -
-
-

取件码生成成功!

-

分享以下信息给接收方

-
-
-
- - {/* 中间区域:取件码 + 分隔线 + 二维码 */} -
- {/* 左侧:取件码 */} -
- -
-
- {pickupCode} -
-
- -
- - {/* 分隔线 - 大屏幕显示竖线,移动端隐藏 */} -
- - {/* 右侧:二维码 */} - {pickupLink && ( -
- -
- -
-
- 使用手机扫码快速访问 -
-
- )} -
- - {/* 底部:取件链接 */} - {pickupLink && ( -
-
-
-
- {pickupLink} -
-
- -
-
- )} -
+ {pickupCode && pickupLink && ( + {})} + onCopyLink={onCopyLink || (() => {})} + /> )}
); diff --git a/chuan-next/src/components/webrtc/WebRTCTextSender.tsx b/chuan-next/src/components/webrtc/WebRTCTextSender.tsx index f96bf00..e87e1c6 100644 --- a/chuan-next/src/components/webrtc/WebRTCTextSender.tsx +++ b/chuan-next/src/components/webrtc/WebRTCTextSender.tsx @@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/toast-simple'; import { MessageSquare, Image, Send, Copy } from 'lucide-react'; import QRCodeDisplay from '@/components/QRCodeDisplay'; +import RoomInfoDisplay from '@/components/RoomInfoDisplay'; interface WebRTCTextSenderProps { onRestart?: () => void; @@ -471,74 +472,24 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o )} - {/* 取件码显示 - 和文件传输一致的风格 */} -
- {/* 左上角状态提示 - 类似已选择文件的风格 */} -
-
-
- -
-
-

取件码生成成功!

-

分享以下信息给接收方

-
-
-
- - {/* 中间区域:取件码 + 分隔线 + 二维码 */} -
- {/* 左侧:取件码 */} -
- -
-
- {pickupCode} -
-
- -
- - {/* 分隔线 - 大屏幕显示竖线,移动端隐藏 */} -
- - {/* 右侧:二维码 */} -
- -
- -
-
- 使用手机扫码快速访问 -
-
-
- - {/* 底部:取件链接 */} -
-
-
-
- {pickupLink} -
-
- -
-
-
+ {/* 取件码显示 */} + )} diff --git a/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts b/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts index f59960a..f8b8bd7 100644 --- a/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts +++ b/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts @@ -29,6 +29,37 @@ export function useDesktopShareBusiness() { setState(prev => ({ ...prev, ...updates })); }, []); + // 处理远程流 + const handleRemoteStream = useCallback((stream: MediaStream) => { + console.log('[DesktopShare] 收到远程流:', stream.getTracks().length, '个轨道'); + updateState({ remoteStream: stream }); + + // 如果有视频元素引用,设置流 + if (remoteVideoRef.current) { + remoteVideoRef.current.srcObject = stream; + } + }, [updateState]); + + // 设置远程轨道处理器(始终监听) + useEffect(() => { + 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); + remoteStream.getTracks().forEach(track => { + console.log('[DesktopShare] 远程轨道:', track.kind, track.id, track.enabled, track.readyState); + }); + handleRemoteStream(remoteStream); + } else { + console.warn('[DesktopShare] ⚠️ 收到轨道但没有关联的流'); + } + }); + }, [webRTC, handleRemoteStream]); + // 生成6位房间代码 const generateRoomCode = useCallback(() => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; @@ -141,17 +172,6 @@ export function useDesktopShareBusiness() { }; }, [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 { @@ -313,21 +333,6 @@ export function useDesktopShareBusiness() { 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) { @@ -336,7 +341,7 @@ export function useDesktopShareBusiness() { updateState({ error: errorMessage, isViewing: false }); throw error; } - }, [webRTC, handleRemoteStream, updateState]); + }, [webRTC, updateState]); // 停止观看桌面共享 const stopViewing = useCallback(async (): Promise => {