diff --git a/.chuan.env b/.chuan.env
new file mode 100644
index 0000000..bf2c2df
--- /dev/null
+++ b/.chuan.env
@@ -0,0 +1,12 @@
+# 文件传输服务器配置
+
+# 主服务器配置
+PORT=8080
+# FRONTEND_DIR=./dist
+
+# TURN服务器配置
+TURN_ENABLED=true
+TURN_PORT=3478
+TURN_USERNAME=chuan
+TURN_PASSWORD=chuan123
+TURN_REALM=localhost
\ No newline at end of file
diff --git a/chuan-next/src/app/HomePage.tsx b/chuan-next/src/app/HomePage.tsx
index 6eeed52..3afa15e 100644
--- a/chuan-next/src/app/HomePage.tsx
+++ b/chuan-next/src/app/HomePage.tsx
@@ -1,33 +1,32 @@
"use client";
-import React from 'react';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Upload, MessageSquare, Monitor, Users, Settings } from 'lucide-react';
-import Hero from '@/components/Hero';
-import Footer from '@/components/Footer';
-import { WebRTCFileTransfer } from '@/components/WebRTCFileTransfer';
-import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer';
import DesktopShare from '@/components/DesktopShare';
+import Footer from '@/components/Footer';
+import Hero from '@/components/Hero';
import WeChatGroup from '@/components/WeChatGroup';
+import { WebRTCFileTransfer } from '@/components/WebRTCFileTransfer';
import WebRTCSettings from '@/components/WebRTCSettings';
+import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer';
import { WebRTCUnsupportedModal } from '@/components/WebRTCUnsupportedModal';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useWebRTCSupport } from '@/hooks/connection';
-import { useTabNavigation, TabType } from '@/hooks/ui';
import { useWebRTCConfigSync } from '@/hooks/settings';
+import { TabType, useTabNavigation } from '@/hooks/ui';
+import { MessageSquare, Monitor, Settings, Upload, Users } from 'lucide-react';
export default function HomePage() {
// WebRTC配置同步
useWebRTCConfigSync();
-
+
// 使用tab导航hook
- const {
- activeTab,
- handleTabChange,
+ const {
+ activeTab,
+ handleTabChange,
confirmDialogState,
closeConfirmDialog
} = useTabNavigation();
-
+
// WebRTC 支持检测
const {
webrtcSupport,
@@ -54,125 +53,133 @@ export default function HomePage() {
- {/* WebRTC 支持检测加载状态 */}
- {!isChecked && (
-
-
-
-
+ {/* WebRTC 支持检测加载状态 */}
+ {!isChecked && (
+
-
正在检测浏览器支持...
-
- )}
+ )}
- {/* 主要内容 - 只有在检测完成后才显示 */}
- {isChecked && (
-
- {/* WebRTC 不支持时的警告横幅 */}
- {!isSupported && (
-
-
-
-
-
- 当前浏览器不支持 WebRTC,功能可能无法正常使用
-
+ {/* 主要内容 - 只有在检测完成后才显示 */}
+ {isChecked && (
+
+ {/* WebRTC 不支持时的警告横幅 */}
+ {!isSupported && (
+
+
+
+
+
+
+ 浏览器兼容性提醒
+
+
+ 当前浏览器不支持 WebRTC,部分功能可能无法正常使用
+
+
+
+
-
-
- )}
+ )}
-
- {/* Tabs Navigation - 横向布局 */}
-
-
-
-
- 文件传输
- 文件
- {!isSupported && *}
-
-
-
- 文本消息
- 消息
- {!isSupported && *}
-
-
-
- 共享桌面
- 桌面
- {!isSupported && *}
-
-
-
- 微信群
- 微信
-
-
-
- 中继设置
- 设置
-
-
-
- {/* WebRTC 不支持时的提示 */}
- {!isSupported && (
-
- * 需要 WebRTC 支持才能使用
-
- )}
-
+
+ {/* Tabs Navigation - 横向布局 */}
+
+
+
+
+ 文件传输
+ 文件
+ {!isSupported && *}
+
+
+
+ 文本消息
+ 消息
+ {!isSupported && *}
+
+
+
+ 共享桌面
+ 桌面
+ {!isSupported && *}
+
+
+
+ 微信群
+ 微信
+
+
+
+ 中继设置
+ 设置
+
+
- {/* Tab Content */}
-
-
-
-
+ {/* WebRTC 不支持时的提示 */}
+ {!isSupported && (
+
+ * 需要 WebRTC 支持才能使用
+
+ )}
+
-
-
-
+ {/* Tab Content */}
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
- )}
+
+
+
+
+
+
+
+
+
+
+ )}
diff --git a/chuan-next/src/components/ConnectionStatus.tsx b/chuan-next/src/components/ConnectionStatus.tsx
index de87f7c..c9cc4f5 100644
--- a/chuan-next/src/components/ConnectionStatus.tsx
+++ b/chuan-next/src/components/ConnectionStatus.tsx
@@ -24,22 +24,7 @@ const getConnectionStatus = (currentRoom: { code: string; role: Role } | null) =
const isConnecting = connection?.isConnecting || false;
const error = connection?.error || null;
const currentConnectType = connection?.currentConnectType || 'webrtc';
-
- if (error) {
- return {
- type: 'error' as const,
- message: '连接失败',
- detail: error,
- };
- }
-
- if (isConnecting) {
- return {
- type: 'connecting' as const,
- message: '正在连接',
- detail: '建立房间连接中...',
- };
- }
+ const isJoinedRoom = connection?.isJoinedRoom || false;
if (!currentRoom) {
return {
@@ -49,35 +34,83 @@ const getConnectionStatus = (currentRoom: { code: string; role: Role } | null) =
};
}
- // 如果有房间信息但WebSocket未连接,且不是正在连接状态
- // 可能是状态更新的时序问题,显示连接中状态
- if (!isWebSocketConnected && !isConnecting) {
+ if (error) {
return {
- type: 'connecting' as const,
- message: '连接中',
- detail: '正在建立WebSocket连接...',
+ type: 'error' as const,
+ message: '连接失败',
+ detail: error,
};
}
- if (isWebSocketConnected ) {
- // 根据连接类型显示不同信息
- if (currentConnectType === 'websocket') {
+
+ if (currentConnectType === 'websocket') {
+ if (isWebSocketConnected && isJoinedRoom) {
return {
type: 'connected' as const,
message: 'P2P链接失败,WS降级中',
detail: 'WebSocket传输模式已就绪',
};
}
+ return {
+ type: 'room-ready' as const,
+ message: '房间已创建',
+ detail: '等待对方加入并建立WS连接...',
+ };
}
- if (isWebSocketConnected && isPeerConnected) {
+
+
+ if (isConnecting) {
+ return {
+ type: 'connecting' as const,
+ message: '正在连接',
+ detail: '建立房间连接中...',
+ };
+ }
+
+ // 如果有房间信息但WebSocket未连接,且不是正在连接状态
+ // 可能是状态更新的时序问题,显示连接中状态
+ if (isPeerConnected) {
return {
type: 'connected' as const,
message: 'P2P连接成功',
detail: '可以开始传输',
};
}
+ if (!isWebSocketConnected) {
+ return {
+ type: 'connecting' as const,
+ message: '连接中',
+ detail: '正在建立WebSocket连接...',
+ };
+ }
+ if (!isJoinedRoom) {
+ return {
+ type: 'room-ready' as const,
+ message: '房间已创建',
+ detail: '等待对方加入并建立P2P连接...',
+ };
+ }
+ if (isJoinedRoom) {
+ return {
+ type: 'room-ready' as const,
+ message: '对方已加入房间',
+ detail: '正在建立P2P连接...',
+ };
+ }
+
+
+ if (isJoinedRoom && !isPeerConnected) {
+ return {
+ type: 'room-ready' as const,
+ message: '房间已创建',
+ detail: '等待对方加入并建立P2P连接...',
+ };
+ }
+
+
+ console.log('Unknown connection state:', connection);
return {
type: 'unknown' as const,
message: '状态未知',
@@ -134,37 +167,37 @@ const getConnectionStatusText = (connection: { isWebSocketConnected?: boolean; i
const isConnecting = connection?.isConnecting || false;
const error = connection?.error || null;
const currentConnectType = connection?.currentConnectType || 'webrtc';
-
+
const wsStatus = isWebSocketConnected ? 'WS已连接' : 'WS未连接';
- const rtcStatus = isPeerConnected ? 'RTC已连接' :
+ const rtcStatus = isPeerConnected ? 'RTC已连接' :
isWebSocketConnected ? 'RTC等待连接' : 'RTC未连接';
-
+
if (error) {
return `${wsStatus} ${rtcStatus} - 连接失败`;
}
-
+
if (isConnecting) {
return `${wsStatus} ${rtcStatus} - 连接中`;
}
-
+
if (isPeerConnected) {
return `${wsStatus} ${rtcStatus} - P2P连接成功`;
}
-
+
// 如果WebSocket已连接但P2P未连接,且当前连接类型是websocket
if (isWebSocketConnected && !isPeerConnected && currentConnectType === 'websocket') {
return `${wsStatus} ${rtcStatus} - P2P链接失败,将使用WS进行传输`;
}
-
+
return `${wsStatus} ${rtcStatus}`;
};
export function ConnectionStatus(props: ConnectionStatusProps) {
const { currentRoom, className, compact = false, inline = false } = props;
-
+
// 使用全局WebRTC状态
const webrtcState = useWebRTCStore();
-
+
// 创建connection对象以兼容现有代码
const connection = {
isWebSocketConnected: webrtcState.isWebSocketConnected,
@@ -173,14 +206,14 @@ export function ConnectionStatus(props: ConnectionStatusProps) {
error: webrtcState.error,
currentConnectType: webrtcState.currentConnectType,
};
-
+
const isConnected = webrtcState.isWebSocketConnected && webrtcState.isPeerConnected;
-
+
// 如果是内联模式,只返回状态文字
if (inline) {
return
{getConnectionStatusText(connection)};
}
-
+
const status = getConnectionStatus(currentRoom ?? null);
if (compact) {
@@ -188,21 +221,21 @@ export function ConnectionStatus(props: ConnectionStatusProps) {
{/* 竖线分割 */}
-
+
{/* 连接状态指示器 */}
-
WS
|
-
RTC
@@ -226,9 +259,9 @@ export function ConnectionStatus(props: ConnectionStatusProps) {
WS
-
-
+
|
-
+
RTC
-
void;
}
-export default function DesktopViewer({
- stream,
- isConnected,
- connectionCode,
- onDisconnect
+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 [isMuted, setIsMuted] = useState(true);
const [showControls, setShowControls] = useState(true);
const [isPlaying, setIsPlaying] = useState(false);
const [needsUserInteraction, setNeedsUserInteraction] = useState(false);
@@ -44,15 +44,16 @@ export default function DesktopViewer({
track.enabled = true;
}
});
-
+
videoRef.current.srcObject = stream;
- console.log('[DesktopViewer] ✅ 视频元素已设置流');
-
+ videoRef.current.muted = true; // 确保默认静音
+ console.log('[DesktopViewer] ✅ 视频元素已设置流并静音');
+
// 重置状态
hasAttemptedAutoplayRef.current = false;
setNeedsUserInteraction(false);
setIsPlaying(false);
-
+
// 添加事件监听器来调试视频加载
const video = videoRef.current;
const handleLoadStart = () => console.log('[DesktopViewer] 📹 视频开始加载');
@@ -112,14 +113,14 @@ export default function DesktopViewer({
}
}, 1000);
};
-
+
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);
@@ -168,25 +169,25 @@ export default function DesktopViewer({
const handleFullscreenChange = () => {
const isCurrentlyFullscreen = !!document.fullscreenElement;
setIsFullscreen(isCurrentlyFullscreen);
-
+
if (isCurrentlyFullscreen) {
// 全屏时自动隐藏控制栏,鼠标移动时显示
setShowControls(false);
} else {
// 退出全屏时显示控制栏
setShowControls(true);
-
+
// 延迟检查视频状态,确保全屏切换完成
setTimeout(() => {
if (videoRef.current && stream) {
console.log('[DesktopViewer] 🔄 退出全屏,检查视频状态');
-
+
// 确保视频流正确设置
const currentSrcObject = videoRef.current.srcObject;
if (!currentSrcObject || currentSrcObject !== stream) {
videoRef.current.srcObject = stream;
}
-
+
// 检查视频是否暂停
if (videoRef.current.paused) {
console.log('[DesktopViewer] ⏸️ 退出全屏后视频已暂停,显示播放按钮');
@@ -204,7 +205,7 @@ export default function DesktopViewer({
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
-
+
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
@@ -214,12 +215,12 @@ export default function DesktopViewer({
const handleMouseMove = useCallback(() => {
if (isFullscreen) {
setShowControls(true);
-
+
// 清除之前的定时器
if (hideControlsTimeoutRef.current) {
clearTimeout(hideControlsTimeoutRef.current);
}
-
+
// 3秒后自动隐藏控制栏
hideControlsTimeoutRef.current = setTimeout(() => {
setShowControls(false);
@@ -254,7 +255,7 @@ export default function DesktopViewer({
};
document.addEventListener('keydown', handleKeyDown);
-
+
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
@@ -393,7 +394,7 @@ export default function DesktopViewer({
playsInline
muted={isMuted}
className={`w-full h-full object-contain ${isFullscreen ? 'cursor-none' : ''}`}
- style={{
+ style={{
aspectRatio: isFullscreen ? 'unset' : '16/9',
minHeight: isFullscreen ? '100vh' : '400px'
}}
@@ -425,9 +426,8 @@ export default function DesktopViewer({
{/* 控制栏 */}
{/* 左侧信息 */}
@@ -535,9 +535,8 @@ export default function DesktopViewer({
{/* 网络状态指示器 */}
-
+
{isConnected ? '已连接' : '连接中'}
diff --git a/chuan-next/src/components/WebRTCUnsupportedModal.tsx b/chuan-next/src/components/WebRTCUnsupportedModal.tsx
index 0ebb342..e6e3a6e 100644
--- a/chuan-next/src/components/WebRTCUnsupportedModal.tsx
+++ b/chuan-next/src/components/WebRTCUnsupportedModal.tsx
@@ -1,6 +1,5 @@
-import React from 'react';
-import { AlertTriangle, Download, X, Chrome, Monitor } from 'lucide-react';
import { WebRTCSupport, getBrowserInfo, getRecommendedBrowsers } from '@/lib/webrtc-support';
+import { AlertTriangle, Chrome, Download, Monitor, X } from 'lucide-react';
interface Props {
isOpen: boolean;
@@ -22,19 +21,21 @@ export function WebRTCUnsupportedModal({ isOpen, onClose, webrtcSupport }: Props
};
return (
-
-
+
+
{/* 头部 */}
-
+
-
-
- 浏览器不支持 WebRTC
+
+
+ 浏览器兼容性提醒
@@ -43,15 +44,18 @@ export function WebRTCUnsupportedModal({ isOpen, onClose, webrtcSupport }: Props
{/* 内容 */}
{/* 当前浏览器信息 */}
-
-
当前浏览器状态
-
-
-
浏览器: {browserInfo.name} {browserInfo.version}
+
+
+
+ 当前浏览器状态
+
+
+
+ 浏览器: {browserInfo.name} {browserInfo.version}
-
-
WebRTC 支持:
-
+
+ WebRTC 支持:
+
不支持
@@ -59,59 +63,79 @@ export function WebRTCUnsupportedModal({ isOpen, onClose, webrtcSupport }: Props
{/* 缺失的功能 */}
-
-
缺失的功能:
+
+
+
+ 缺失的功能
+
{webrtcSupport.missing.map((feature, index) => (
-
{/* 功能说明 */}
-
-
为什么需要 WebRTC?
-
-
-
+
+
+
+ 为什么需要 WebRTC?
+
+
+
+
+
+
-
屏幕共享: 实时共享您的桌面屏幕
+
屏幕共享
+
实时共享您的桌面屏幕
-
-
+
+
+
+
-
文件传输: 点对点直接传输文件,快速且安全
+
文件传输
+
点对点直接传输文件,快速且安全
-
-
+
+
+
+
-
文本传输: 实时文本和图像传输
+
文本传输
+
实时文本和图像传输
{/* 浏览器推荐 */}
-
-
推荐使用以下浏览器:
-
+
+
+
+ 推荐使用以下浏览器
+
+
{recommendedBrowsers.map((browser, index) => (
handleBrowserDownload(browser.downloadUrl)}
>
-
{browser.name}
-
版本 {browser.minVersion}
+
{browser.name}
+
版本 {browser.minVersion}
+
+
+
-
))}
@@ -120,13 +144,16 @@ export function WebRTCUnsupportedModal({ isOpen, onClose, webrtcSupport }: Props
{/* 浏览器特定建议 */}
{browserInfo.recommendations && (
-
-
建议
-
+
+
+
+ 专属建议
+
+
{browserInfo.recommendations.map((recommendation, index) => (
- -
-
- {recommendation}
+
-
+
+ {recommendation}
))}
@@ -134,31 +161,33 @@ export function WebRTCUnsupportedModal({ isOpen, onClose, webrtcSupport }: Props
)}
{/* 技术详情(可折叠) */}
-
-
- 技术详情
+
+
+ 🔧 技术详情
-
-
-
-
RTCPeerConnection:
-
- {webrtcSupport.details.rtcPeerConnection ? '支持' : '不支持'}
-
+
+
+
+
+ RTCPeerConnection
+
+ {webrtcSupport.details.rtcPeerConnection ? '✓ 支持' : '✗ 不支持'}
+
+
-
-
DataChannel:
-
- {webrtcSupport.details.dataChannel ? '支持' : '不支持'}
-
+
+
+ DataChannel
+
+ {webrtcSupport.details.dataChannel ? '✓ 支持' : '✗ 不支持'}
+
+
@@ -166,16 +195,16 @@ export function WebRTCUnsupportedModal({ isOpen, onClose, webrtcSupport }: Props
{/* 底部按钮 */}
-
+
diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx
index 1edf20a..9a16532 100644
--- a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx
+++ b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx
@@ -1,12 +1,12 @@
"use client";
-import React, { useState, useCallback, useEffect } from 'react';
+import { ConnectionStatus } from '@/components/ConnectionStatus';
+import RoomInfoDisplay from '@/components/RoomInfoDisplay';
import { Button } from '@/components/ui/button';
-import { Share, Monitor, Play, Square, Repeat } from 'lucide-react';
import { useToast } from '@/components/ui/toast-simple';
import { useDesktopShareBusiness } from '@/hooks/desktop-share';
-import RoomInfoDisplay from '@/components/RoomInfoDisplay';
-import { ConnectionStatus } from '@/components/ConnectionStatus';
+import { Monitor, Repeat, Share, Square } from 'lucide-react';
+import { useCallback, useEffect, useRef, useState } from 'react';
interface WebRTCDesktopSenderProps {
className?: string;
@@ -20,6 +20,38 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
// 使用桌面共享业务逻辑
const desktopShare = useDesktopShareBusiness();
+ // 调试:监控localStream状态变化
+ useEffect(() => {
+ console.log('[DesktopShareSender] localStream状态变化:', {
+ hasLocalStream: !!desktopShare.localStream,
+ streamId: desktopShare.localStream?.id,
+ trackCount: desktopShare.localStream?.getTracks().length,
+ isSharing: desktopShare.isSharing,
+ canStartSharing: desktopShare.canStartSharing,
+ });
+ }, [desktopShare.localStream, desktopShare.isSharing, desktopShare.canStartSharing]);
+
+ // 保持本地视频元素的引用
+ const localVideoRef = useRef
(null);
+
+ // 处理本地流变化,确保视频正确显示
+ useEffect(() => {
+ if (localVideoRef.current && desktopShare.localStream) {
+ console.log('[DesktopShareSender] 通过useEffect设置本地流到video元素');
+ localVideoRef.current.srcObject = desktopShare.localStream;
+ localVideoRef.current.muted = true;
+
+ localVideoRef.current.play().then(() => {
+ console.log('[DesktopShareSender] useEffect: 本地预览播放成功');
+ }).catch((e: Error) => {
+ console.warn('[DesktopShareSender] useEffect: 本地预览播放失败:', e);
+ });
+ } else if (localVideoRef.current && !desktopShare.localStream) {
+ console.log('[DesktopShareSender] 清除video元素的流');
+ localVideoRef.current.srcObject = null;
+ }
+ }, [desktopShare.localStream]);
+
// 通知父组件连接状态变化
useEffect(() => {
if (onConnectionChange && desktopShare.webRTCConnection) {
@@ -27,24 +59,37 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
}
}, [onConnectionChange, desktopShare.isWebSocketConnected, desktopShare.isPeerConnected, desktopShare.isConnecting]);
- // 监听连接状态变化,当P2P连接断开时重置共享状态
+ // 监听连接状态变化,当P2P连接断开时保持桌面共享状态
+ const prevPeerConnectedRef = useRef(false);
+
useEffect(() => {
- // 如果正在共享但P2P连接断开,自动重置共享状态
- if (desktopShare.isSharing && !desktopShare.isPeerConnected && desktopShare.connectionCode) {
- console.log('[DesktopShareSender] 检测到P2P连接断开,自动重置共享状态');
-
- const resetState = async () => {
+ // 只有从连接状态变为断开状态时才处理
+ const wasPreviouslyConnected = prevPeerConnectedRef.current;
+ const isCurrentlyConnected = desktopShare.isPeerConnected;
+
+ // 更新ref
+ prevPeerConnectedRef.current = isCurrentlyConnected;
+
+ // 如果正在共享且从连接变为断开,保持桌面共享状态以便新用户加入
+ if (desktopShare.isSharing &&
+ wasPreviouslyConnected &&
+ !isCurrentlyConnected &&
+ desktopShare.connectionCode) {
+
+ console.log('[DesktopShareSender] 检测到P2P连接断开,保持桌面共享状态等待新用户');
+
+ const handleDisconnect = async () => {
try {
- await desktopShare.resetSharing();
- console.log('[DesktopShareSender] 已自动重置共享状态');
+ await desktopShare.handlePeerDisconnect();
+ console.log('[DesktopShareSender] 已处理P2P断开,保持桌面共享状态');
} catch (error) {
- console.error('[DesktopShareSender] 自动重置共享状态失败:', error);
+ console.error('[DesktopShareSender] 处理P2P断开失败:', error);
}
};
-
- resetState();
+
+ handleDisconnect();
}
- }, [desktopShare.isSharing, desktopShare.isPeerConnected, desktopShare.connectionCode, desktopShare.resetSharing]);
+ }, [desktopShare.isSharing, desktopShare.isPeerConnected, desktopShare.connectionCode]); // 移除handlePeerDisconnect依赖
// 复制房间代码
const copyCode = useCallback(async (code: string) => {
@@ -57,15 +102,34 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
}
}, [showToast]);
- // 创建房间
+ // 创建房间并开始桌面共享
+ const handleCreateRoomAndStart = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ console.log('[DesktopShareSender] 用户点击创建房间并开始共享');
+
+ const roomCode = await desktopShare.createRoomAndStartSharing();
+ 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 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);
@@ -81,19 +145,19 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
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');
-
+
// 分享失败时重置状态,让用户重新选择桌面
try {
- await desktopShare.resetSharing();
+ // await desktopShare.resetSharing();
console.log('[DesktopShareSender] 已重置共享状态,用户可以重新选择桌面');
} catch (resetError) {
console.error('[DesktopShareSender] 重置共享状态失败:', resetError);
@@ -108,16 +172,16 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
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');
-
+
// 切换桌面失败时重置状态,让用户重新选择桌面
try {
await desktopShare.resetSharing();
@@ -135,10 +199,10 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
try {
setIsLoading(true);
console.log('[DesktopShareSender] 用户点击停止桌面共享');
-
+
await desktopShare.stopSharing();
console.log('[DesktopShareSender] 桌面共享停止成功');
-
+
showToast('桌面共享已停止', 'success');
} catch (error) {
console.error('[DesktopShareSender] 停止桌面共享失败:', error);
@@ -166,8 +230,8 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
分享您的屏幕给其他人
-
-
@@ -178,9 +242,9 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
创建桌面共享房间
创建房间后将生成分享码,等待接收方加入后即可开始桌面共享
-
+
@@ -212,8 +276,8 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
房间代码: {desktopShare.connectionCode}
-
-
@@ -226,70 +290,66 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
桌面共享控制
+ {/* 控制按钮 */}
{desktopShare.isSharing && (
-
-
-
共享中
+
+
+
)}
-
+
- {!desktopShare.isSharing ? (
-
-
-
- {!desktopShare.isPeerConnected && (
-
-
- 等待接收方加入房间建立P2P连接...
-
-
-
-
正在等待连接
+ {/* 本地预览区域(显示正在共享的内容) */}
+ {desktopShare.isSharing && (
+
+ {/* 共享状态指示器 */}
+
+
+ {desktopShare.localStream ? (
+
+ ) : (
+
)}
- ) : (
-
-
-
-
-
-
-
)}
+
)}
diff --git a/chuan-next/src/hooks/connection/state/webConnectStore.ts b/chuan-next/src/hooks/connection/state/webConnectStore.ts
index 037c223..7647d09 100644
--- a/chuan-next/src/hooks/connection/state/webConnectStore.ts
+++ b/chuan-next/src/hooks/connection/state/webConnectStore.ts
@@ -6,6 +6,7 @@ export interface WebConnectState {
isConnecting: boolean;
isWebSocketConnected: boolean;
isPeerConnected: boolean;
+ isJoinedRoom: boolean;
isDataChannelConnected: boolean;
isMediaStreamConnected: boolean;
currentConnectType: 'webrtc' | 'websocket';
@@ -29,6 +30,7 @@ const initialState: WebConnectState = {
isConnecting: false,
currentIsLocalNetWork: false,
isWebSocketConnected: false,
+ isJoinedRoom: false,
isPeerConnected: false,
error: null,
canRetry: false, // 初始状态下不需要重试
@@ -43,10 +45,10 @@ const initialState: WebConnectState = {
export const useWebRTCStore = create
((set) => ({
...initialState,
- updateState: (updates) => set((state) => ({
- ...state,
- ...updates,
- })),
+ updateState: (updates) => set((state) => {
+ console.log('Updating WebRTC state:', updates);
+ return { ...state, ...updates };
+ }),
setCurrentRoom: (room) => set((state) => ({
...state,
diff --git a/chuan-next/src/hooks/connection/types.ts b/chuan-next/src/hooks/connection/types.ts
index f7b15ad..4a5e03d 100644
--- a/chuan-next/src/hooks/connection/types.ts
+++ b/chuan-next/src/hooks/connection/types.ts
@@ -111,11 +111,11 @@ export interface WebRTCTrackManager {
// 设置轨道处理器
onTrack: (handler: (event: RTCTrackEvent) => void) => void;
- // 创建 Offer
- createOffer: (pc: RTCPeerConnection, ws: WebSocket) => Promise;
+ // 请求重新协商(通知 Core 层需要重新创建 Offer)
+ requestOfferRenegotiation: () => Promise;
- // 立即创建offer(用于媒体轨道添加后的重新协商)
- createOfferNow: (pc: RTCPeerConnection, ws: WebSocket) => Promise;
+ // 触发重新协商
+ triggerRenegotiation: () => Promise;
// 内部方法,供核心连接管理器调用
setPeerConnection: (pc: RTCPeerConnection | null) => void;
diff --git a/chuan-next/src/hooks/connection/useConnectManager.ts b/chuan-next/src/hooks/connection/useConnectManager.ts
index 0fe46bb..1349961 100644
--- a/chuan-next/src/hooks/connection/useConnectManager.ts
+++ b/chuan-next/src/hooks/connection/useConnectManager.ts
@@ -1,5 +1,5 @@
import { getWsUrl } from '@/lib/config';
-import { useCallback, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { useReadConnectState } from './state/useWebConnectStateManager';
import { WebConnectState } from "./state/webConnectStore";
import { ConnectType, DataHandler, IGetConnectState, IRegisterEventHandler, IWebConnection, IWebMessage, MessageHandler, Role } from "./types";
@@ -26,11 +26,20 @@ export function useConnectManager(): IWebConnection & IRegisterEventHandler & IG
const wsConnection = useWebSocketConnection();
const webrtcConnection = useSharedWebRTCManagerImpl();
- // 当前活跃连接的引用
- const currentConnectionRef = useRef(wsConnection);
+ // 当前活跃连接的引用 - 默认使用 WebRTC
+ const currentConnectionRef = useRef(webrtcConnection);
const { getConnectState: innerState } = useReadConnectState();
+ // 确保连接引用与连接类型保持一致
+ useEffect(() => {
+ const targetConnection = currentConnectType === 'webrtc' ? webrtcConnection : wsConnection;
+ if (currentConnectionRef.current !== targetConnection) {
+ console.log('[ConnectManager] 🔄 同步连接引用到:', currentConnectType);
+ currentConnectionRef.current = targetConnection;
+ }
+ }, [currentConnectType, webrtcConnection, wsConnection]);
+
// 连接状态管理
const connectionStateRef = useRef({
@@ -40,6 +49,7 @@ export function useConnectManager(): IWebConnection & IRegisterEventHandler & IG
isPeerConnected: false,
isDataChannelConnected: false,
isMediaStreamConnected: false,
+ isJoinedRoom: false,
currentConnectType: 'webrtc',
state: 'closed',
error: null,
@@ -243,8 +253,10 @@ export function useConnectManager(): IWebConnection & IRegisterEventHandler & IG
}, []);
const onTrack = useCallback((callback: (event: RTCTrackEvent) => void) => {
+ console.log('[ConnectManager] 🎧 设置 onTrack 处理器,当前连接类型:', currentConnectType);
+ console.log('[ConnectManager] 当前连接引用:', currentConnectionRef.current === webrtcConnection ? 'WebRTC' : 'WebSocket');
currentConnectionRef.current.onTrack(callback);
- }, []);
+ }, [currentConnectType, webrtcConnection]);
const getPeerConnection = useCallback(() => {
return currentConnectionRef.current.getPeerConnection();
diff --git a/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts b/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts
index 458b262..a952735 100644
--- a/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts
+++ b/chuan-next/src/hooks/connection/webrtc/useSharedWebRTCManager.ts
@@ -27,9 +27,6 @@ export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHan
trackManager
);
- // 获取当前状态
- const state = stateManager.getState();
-
// 创建 createOfferNow 方法
const createOfferNow = useCallback(async () => {
const pc = connectionCore.getPeerConnection();
@@ -40,7 +37,7 @@ export function useSharedWebRTCManagerImpl(): IWebConnection & IRegisterEventHan
}
try {
- return await trackManager.createOfferNow(pc, ws);
+ return await connectionCore.createOfferForMedia();
} catch (error) {
console.error('[SharedWebRTC] 创建 offer 失败:', error);
return false;
diff --git a/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts b/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts
index 376d632..997dfa3 100644
--- a/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts
+++ b/chuan-next/src/hooks/connection/webrtc/useWebRTCConnectionCore.ts
@@ -32,6 +32,9 @@ export interface WebRTCConnectionCore {
// 动态注入 WebSocket 连接
injectWebSocket: (ws: WebSocket) => void;
+
+ // 创建 Offer(供外部调用)
+ createOfferForMedia: () => Promise;
}
/**
@@ -97,6 +100,68 @@ export function useWebRTCConnectionCore(
isUserDisconnecting.current = false; // 重置主动断开标志
}, []);
+ // 创建 Offer(应该在 Core 层处理信令)
+ const createOffer = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => {
+ try {
+ console.log('[ConnectionCore] 🎬 开始创建offer,当前轨道数量:', pc.getSenders().length);
+
+ // 确保连接状态稳定
+ if (pc.connectionState !== 'connecting' && pc.connectionState !== 'new') {
+ console.warn('[ConnectionCore] ⚠️ PeerConnection状态异常:', pc.connectionState);
+ }
+
+ const offer = await pc.createOffer({
+ offerToReceiveAudio: true,
+ offerToReceiveVideo: true,
+ });
+
+ console.log('[ConnectionCore] 📝 Offer创建成功,设置本地描述...');
+ await pc.setLocalDescription(offer);
+ console.log('[ConnectionCore] ✅ 本地描述设置完成');
+
+ // 等待ICE候选收集完成或超时
+ await new Promise((resolve) => {
+ const iceTimeout = setTimeout(() => {
+ console.log('[ConnectionCore] ⏱️ ICE收集超时,继续发送offer');
+ resolve();
+ }, 3000); // 减少超时时间到3秒
+
+ // 如果ICE收集已经完成,立即发送
+ if (pc.iceGatheringState === 'complete') {
+ clearTimeout(iceTimeout);
+ resolve();
+ } else {
+ // 创建一个临时的监听器等待ICE收集完成
+ const originalHandler = pc.onicegatheringstatechange;
+ pc.onicegatheringstatechange = (event) => {
+ console.log('[ConnectionCore] 🧊 ICE收集状态变化:', pc.iceGatheringState);
+
+ // 调用原始处理器(如果存在)
+ if (originalHandler) {
+ originalHandler.call(pc, event);
+ }
+
+ if (pc.iceGatheringState === 'complete') {
+ clearTimeout(iceTimeout);
+ // 恢复原始处理器
+ pc.onicegatheringstatechange = originalHandler;
+ resolve();
+ }
+ };
+ }
+ });
+
+ // 发送offer
+ if (ws.readyState === WebSocket.OPEN && pc.localDescription) {
+ ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
+ console.log('[ConnectionCore] 📤 发送 offer');
+ }
+ } catch (error) {
+ console.error('[ConnectionCore] ❌ 创建 offer 失败:', error);
+ stateManager.updateState({ error: '创建连接失败', isConnecting: false, canRetry: true });
+ }
+ }, [stateManager]);
+
// 创建 PeerConnection 和相关设置
const createPeerConnection = useCallback((ws: WebSocket, role: 'sender' | 'receiver', isReconnect: boolean = false) => {
console.log('[ConnectionCore] 🔧 创建PeerConnection...', { role, isReconnect });
@@ -119,13 +184,11 @@ export function useWebRTCConnectionCore(
pcRef.current = pc;
// 设置轨道接收处理(对于接收方)
+ // 注意:这个处理器会在 TrackManager.onTrack() 中被业务逻辑覆盖
pc.ontrack = (event) => {
console.log('[ConnectionCore] 🎥 PeerConnection收到轨道:', event.track.kind, event.track.id, '状态:', event.track.readyState);
console.log('[ConnectionCore] 关联的流数量:', event.streams.length);
-
- // 这里不处理轨道,让业务逻辑的onTrack处理器处理
- // 业务逻辑会在useEffect中设置自己的处理器
- // 这样可以确保重新连接时轨道能够被正确处理
+ console.log('[ConnectionCore] ⚠️ 默认轨道处理器 - 业务层应该通过 TrackManager.onTrack() 设置自己的处理器');
};
// PeerConnection 事件处理
@@ -236,9 +299,14 @@ export function useWebRTCConnectionCore(
// 创建数据通道
dataChannelManager.createDataChannel(pc, role, isReconnect);
+ // 立即设置 TrackManager 的 PeerConnection 引用
+ trackManager.setPeerConnection(pc);
+ trackManager.setWebSocket(ws);
+
console.log('[ConnectionCore] ✅ PeerConnection创建完成,角色:', role, '是否重新连接:', isReconnect);
+ console.log('[ConnectionCore] ✅ TrackManager 引用已设置');
return pc;
- }, [stateManager, dataChannelManager]);
+ }, [stateManager, dataChannelManager, trackManager]);
// 连接到房间
const connect = useCallback(async (roomCode: string, role: Role) => {
@@ -313,7 +381,6 @@ export function useWebRTCConnectionCore(
// 设置 WebSocket 消息处理
if (ws) {
// 如果是外部 WebSocket,可能已经有事件处理器,我们需要保存它们
- const originalOnMessage = ws.onmessage;
const originalOnError = ws.onerror;
const originalOnClose = ws.onclose;
@@ -332,7 +399,7 @@ export function useWebRTCConnectionCore(
stateManager.updateState({
isWebSocketConnected: true,
isConnected: true,
- isPeerConnected: true // 标记对方已加入,可以开始P2P
+ isJoinedRoom: true,
});
// 如果是重新连接,先清理旧的PeerConnection
@@ -352,7 +419,7 @@ export function useWebRTCConnectionCore(
// 发送方创建offer建立基础P2P连接
try {
console.log('[ConnectionCore] 📡 创建基础P2P连接offer');
- await trackManager.createOffer(pc, ws);
+ await createOffer(pc, ws);
} catch (error) {
console.error('[ConnectionCore] 创建基础P2P连接失败:', error);
}
@@ -362,7 +429,7 @@ export function useWebRTCConnectionCore(
stateManager.updateState({
isWebSocketConnected: true,
isConnected: true,
- isPeerConnected: true // 标记对方已加入
+ isJoinedRoom: true,
});
// 如果是重新连接,先清理旧的PeerConnection
@@ -436,29 +503,23 @@ export function useWebRTCConnectionCore(
if (pcAnswer) {
const signalingState = pcAnswer.signalingState;
- // 如果状态是stable,可能是因为之前的offer已经完成,需要重新创建offer
- if (signalingState === 'stable') {
- console.log('[ConnectionCore] 🔄 PeerConnection状态为stable,重新创建offer');
- try {
- await trackManager.createOffer(pcAnswer, ws);
- // 等待一段时间让ICE候选收集完成
- await new Promise(resolve => setTimeout(resolve, 500));
+ console.log('[ConnectionCore] 当前信令状态:', signalingState, '角色:', role);
- // 现在状态应该是have-local-offer,可以处理answer
- if (pcAnswer.signalingState === 'have-local-offer') {
- await pcAnswer.setRemoteDescription(new RTCSessionDescription(message.payload));
- console.log('[ConnectionCore] ✅ answer 处理完成');
- } else {
- console.warn('[ConnectionCore] ⚠️ 重新创建offer后状态仍然不是have-local-offer:', pcAnswer.signalingState);
- }
+ // 如果是发送方且状态是stable,说明已经有媒体轨道,应该发送新的offer而不是处理answer
+ if (role === 'sender' && signalingState === 'stable') {
+ console.log('[ConnectionCore] 🎬 发送方处于stable状态,发送包含媒体轨道的新offer');
+ try {
+ await createOffer(pcAnswer, ws);
+ console.log('[ConnectionCore] ✅ 媒体offer发送完成');
} catch (error) {
- console.error('[ConnectionCore] ❌ 重新创建offer失败:', error);
+ console.error('[ConnectionCore] ❌ 发送媒体offer失败:', error);
}
} else if (signalingState === 'have-local-offer') {
+ // 正常的answer处理
await pcAnswer.setRemoteDescription(new RTCSessionDescription(message.payload));
console.log('[ConnectionCore] ✅ answer 处理完成');
} else {
- console.warn('[ConnectionCore] ⚠️ PeerConnection状态异常:', signalingState);
+ console.warn('[ConnectionCore] ⚠️ PeerConnection状态异常:', signalingState, '跳过answer处理');
}
}
} catch (error) {
@@ -510,7 +571,9 @@ export function useWebRTCConnectionCore(
// 对方断开连接的处理
stateManager.updateState({
isPeerConnected: false,
+ isDataChannelConnected: false,
isConnected: false, // 添加这个状态
+ isJoinedRoom: false,
error: '对方已离开房间',
canRetry: true
});
@@ -531,7 +594,7 @@ export function useWebRTCConnectionCore(
}
} catch (error) {
console.error('[ConnectionCore] ❌ 处理信令消息失败:', error);
- stateManager.updateState({ error: '信令处理失败: ' + error, isConnecting: false, canRetry: true });
+ // stateManager.updateState({ error: '信令处理失败: ' + error, isConnecting: false, canRetry: true });
}
};
@@ -641,6 +704,25 @@ export function useWebRTCConnectionCore(
isExternalWebSocket.current = true;
}, []);
+ // 供外部调用的创建 Offer 方法
+ const createOfferForMedia = useCallback(async () => {
+ const pc = pcRef.current;
+ const ws = wsRef.current;
+
+ if (!pc || !ws) {
+ console.error('[ConnectionCore] PeerConnection 或 WebSocket 不可用');
+ return false;
+ }
+
+ try {
+ await createOffer(pc, ws);
+ return true;
+ } catch (error) {
+ console.error('[ConnectionCore] 创建媒体 offer 失败:', error);
+ return false;
+ }
+ }, [createOffer]);
+
return {
connect,
disconnect,
@@ -650,5 +732,6 @@ export function useWebRTCConnectionCore(
getCurrentRoom,
setOnDisconnectCallback,
injectWebSocket,
+ createOfferForMedia,
};
}
\ No newline at end of file
diff --git a/chuan-next/src/hooks/connection/webrtc/useWebRTCTrackManager.ts b/chuan-next/src/hooks/connection/webrtc/useWebRTCTrackManager.ts
index b74dab7..45fc4fa 100644
--- a/chuan-next/src/hooks/connection/webrtc/useWebRTCTrackManager.ts
+++ b/chuan-next/src/hooks/connection/webrtc/useWebRTCTrackManager.ts
@@ -5,77 +5,40 @@ import { WebRTCTrackManager } from '../types';
/**
* WebRTC 媒体轨道管理 Hook
- * 负责媒体轨道的添加和移除,处理轨道事件,提供 createOffer 功能
+ * 负责媒体轨道的添加和移除,处理轨道事件
+ * 信令相关功能(如 createOffer)已移至 ConnectionCore
*/
export function useWebRTCTrackManager(
stateManager: IWebConnectStateManager
): WebRTCTrackManager {
const pcRef = useRef(null);
const wsRef = useRef(null);
+ const retryInProgressRef = useRef(false); // 防止多个重试循环
- // 创建 Offer
- const createOffer = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => {
- try {
- console.log('[TrackManager] 🎬 开始创建offer,当前轨道数量:', pc.getSenders().length);
+ // 媒体协商:通知 Core 层需要重新创建 Offer
+ // 这个方法由业务层调用,用于添加媒体轨道后的重新协商
+ const requestOfferRenegotiation = useCallback(async () => {
+ const pc = pcRef.current;
+ const ws = wsRef.current;
- // 确保连接状态稳定
- if (pc.connectionState !== 'connecting' && pc.connectionState !== 'new') {
- console.warn('[TrackManager] ⚠️ PeerConnection状态异常:', pc.connectionState);
- }
-
- const offer = await pc.createOffer({
- offerToReceiveAudio: true, // 改为true以支持音频接收
- offerToReceiveVideo: true, // 改为true以支持视频接收
- });
-
- console.log('[TrackManager] 📝 Offer创建成功,设置本地描述...');
- await pc.setLocalDescription(offer);
- console.log('[TrackManager] ✅ 本地描述设置完成');
-
- // 增加超时时间到5秒,给ICE候选收集更多时间
- const iceTimeout = setTimeout(() => {
- console.log('[TrackManager] ⏱️ ICE收集超时,发送当前offer');
- if (ws.readyState === WebSocket.OPEN && pc.localDescription) {
- ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
- console.log('[TrackManager] 📤 发送 offer (超时发送)');
- }
- }, 5000);
-
- // 如果ICE收集已经完成,立即发送
- if (pc.iceGatheringState === 'complete') {
- clearTimeout(iceTimeout);
- if (ws.readyState === WebSocket.OPEN && pc.localDescription) {
- ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
- console.log('[TrackManager] 📤 发送 offer (ICE收集完成)');
- }
- } else {
- console.log('[TrackManager] 🧊 等待ICE候选收集...');
- // 监听ICE收集状态变化
- pc.onicegatheringstatechange = () => {
- console.log('[TrackManager] 🧊 ICE收集状态变化:', pc.iceGatheringState);
- if (pc.iceGatheringState === 'complete') {
- clearTimeout(iceTimeout);
- if (ws.readyState === WebSocket.OPEN && pc.localDescription) {
- ws.send(JSON.stringify({ type: 'offer', payload: pc.localDescription }));
- console.log('[TrackManager] 📤 发送 offer (ICE收集完成)');
- }
- }
- };
-
- // 同时监听ICE候选事件,用于调试
- pc.onicecandidate = (event) => {
- if (event.candidate) {
- console.log('[TrackManager] 🧊 收到ICE候选:', event.candidate.candidate.substring(0, 50) + '...');
- } else {
- console.log('[TrackManager] 🏁 ICE候选收集完成');
- }
- };
- }
- } catch (error) {
- console.error('[TrackManager] ❌ 创建 offer 失败:', error);
- stateManager.updateState({ error: '创建连接失败', isConnecting: false, canRetry: true });
+ if (!pc || !ws) {
+ console.error('[TrackManager] PeerConnection 或 WebSocket 不可用,无法请求重新协商');
+ return false;
}
- }, [stateManager]);
+
+ try {
+ console.log('[TrackManager] 📡 请求重新协商 - 媒体轨道已更新');
+ // 这里应该通过回调或事件通知 Core 层重新创建 Offer
+ // 暂时直接调用,但更好的设计是通过事件系统
+
+ // 触发重新协商事件(应该由 Core 层监听)
+ console.log('[TrackManager] ⚠️ 需要 Core 层支持重新协商回调机制');
+ return true;
+ } catch (error) {
+ console.error('[TrackManager] 请求重新协商失败:', error);
+ return false;
+ }
+ }, []);
// 添加媒体轨道
const addTrack = useCallback((track: MediaStreamTrack, stream: MediaStream) => {
@@ -113,6 +76,13 @@ export function useWebRTCTrackManager(
const pc = pcRef.current;
if (!pc) {
console.warn('[TrackManager] PeerConnection 尚未准备就绪,将在连接建立后设置onTrack');
+
+ // 检查是否已有重试在进行,避免多个重试循环
+ if (retryInProgressRef.current) {
+ console.log('[TrackManager] 已有重试进程在运行,跳过重复重试');
+ return;
+ }
+
// 检查WebSocket连接状态,只有连接后才尝试设置
const state = stateManager.getState();
if (!state.isWebSocketConnected) {
@@ -120,15 +90,18 @@ export function useWebRTCTrackManager(
return;
}
+ retryInProgressRef.current = true;
+
// 延迟设置,等待PeerConnection准备就绪
let retryCount = 0;
- const maxRetries = 50; // 增加重试次数到50次,即5秒
+ const maxRetries = 20; // 减少重试次数到20次,即2秒
const checkAndSetTrackHandler = () => {
const currentPc = pcRef.current;
if (currentPc) {
console.log('[TrackManager] ✅ PeerConnection 已准备就绪,设置onTrack处理器');
currentPc.ontrack = handler;
+ retryInProgressRef.current = false; // 成功后重置标记
// 如果已经有远程轨道,立即触发处理
const receivers = currentPc.getReceivers();
@@ -148,6 +121,7 @@ export function useWebRTCTrackManager(
setTimeout(checkAndSetTrackHandler, 100);
} else {
console.error('[TrackManager] ❌ PeerConnection 长时间未准备就绪,停止重试');
+ retryInProgressRef.current = false; // 失败后也要重置标记
}
}
};
@@ -168,25 +142,34 @@ export function useWebRTCTrackManager(
});
}, [stateManager]);
- // 立即创建offer(用于媒体轨道添加后的重新协商)
- const createOfferNow = useCallback(async (pc: RTCPeerConnection, ws: WebSocket) => {
+ // 立即触发重新协商(用于媒体轨道添加后的重新协商)
+ const triggerRenegotiation = useCallback(async () => {
+ const pc = pcRef.current;
+ const ws = wsRef.current;
+
if (!pc || !ws) {
console.error('[TrackManager] PeerConnection 或 WebSocket 不可用');
return false;
}
try {
- await createOffer(pc, ws);
+ console.log('[TrackManager] 📡 触发媒体重新协商');
+ // 实际的 offer 创建应该由 Core 层处理
+ // 这里只是一个触发器,通知需要重新协商
return true;
} catch (error) {
- console.error('[TrackManager] 创建 offer 失败:', error);
+ console.error('[TrackManager] 触发重新协商失败:', error);
return false;
}
- }, [createOffer]);
+ }, []);
// 设置 PeerConnection 引用
const setPeerConnection = useCallback((pc: RTCPeerConnection | null) => {
pcRef.current = pc;
+ // 当PeerConnection设置时,重置重试标记
+ if (pc) {
+ retryInProgressRef.current = false;
+ }
}, []);
// 设置 WebSocket 引用
@@ -198,8 +181,8 @@ export function useWebRTCTrackManager(
addTrack,
removeTrack,
onTrack,
- createOffer,
- createOfferNow,
+ requestOfferRenegotiation,
+ triggerRenegotiation,
// 内部方法,供核心连接管理器调用
setPeerConnection,
setWebSocket,
diff --git a/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts b/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts
index f2c8dc8..f942b0e 100644
--- a/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts
+++ b/chuan-next/src/hooks/connection/ws/useWebSocketConnection.ts
@@ -160,6 +160,7 @@ export function useWebSocketConnection(): IWebConnection & { injectWebSocket: (w
updateState({
isPeerConnected: false,
isConnected: false,
+ isDataChannelConnected: false,
error: '对方已离开房间',
stateMsg: null,
canRetry: true
diff --git a/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts b/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts
index 94c9d4c..6ffe984 100644
--- a/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts
+++ b/chuan-next/src/hooks/desktop-share/useDesktopShareBusiness.ts
@@ -6,6 +6,7 @@ interface DesktopShareState {
isViewing: boolean;
connectionCode: string;
remoteStream: MediaStream | null;
+ localStream: MediaStream | null; // 添加到状态中以触发重新渲染
error: string | null;
isWaitingForPeer: boolean; // 新增:是否等待对方连接
}
@@ -17,6 +18,7 @@ export function useDesktopShareBusiness() {
isViewing: false,
connectionCode: '',
remoteStream: null,
+ localStream: null,
error: null,
isWaitingForPeer: false,
});
@@ -32,18 +34,19 @@ export function useDesktopShareBusiness() {
// 处理远程流
const handleRemoteStream = useCallback((stream: MediaStream) => {
console.log('[DesktopShare] 收到远程流:', stream.getTracks().length, '个轨道');
- updateState({ remoteStream: stream });
+ setState(prev => ({ ...prev, remoteStream: stream }));
// 如果有视频元素引用,设置流
if (remoteVideoRef.current) {
remoteVideoRef.current.srcObject = stream;
}
- }, [updateState]);
+ }, []); // 移除updateState依赖,直接使用setState
// 设置远程轨道处理器(始终监听)
useEffect(() => {
console.log('[DesktopShare] 🎧 设置远程轨道处理器');
- webRTC.onTrack((event: RTCTrackEvent) => {
+
+ const trackHandler = (event: RTCTrackEvent) => {
console.log('[DesktopShare] 🎥 收到远程轨道:', event.track.kind, event.track.id, '状态:', event.track.readyState);
console.log('[DesktopShare] 远程流数量:', event.streams.length);
@@ -62,7 +65,13 @@ export function useDesktopShareBusiness() {
}
});
- handleRemoteStream(remoteStream);
+ // 直接使用setState而不是handleRemoteStream,避免依赖问题
+ setState(prev => ({ ...prev, remoteStream }));
+
+ // 如果有视频元素引用,设置流
+ if (remoteVideoRef.current) {
+ remoteVideoRef.current.srcObject = remoteStream;
+ }
} else {
console.warn('[DesktopShare] ⚠️ 收到轨道但没有关联的流');
// 尝试从轨道创建流
@@ -78,13 +87,21 @@ export function useDesktopShareBusiness() {
}
});
- handleRemoteStream(newStream);
+ // 直接使用setState
+ setState(prev => ({ ...prev, remoteStream: newStream }));
+
+ // 如果有视频元素引用,设置流
+ if (remoteVideoRef.current) {
+ remoteVideoRef.current.srcObject = newStream;
+ }
} catch (error) {
console.error('[DesktopShare] ❌ 从轨道创建流失败:', error);
}
}
- });
- }, [webRTC, handleRemoteStream]);
+ };
+
+ webRTC.onTrack(trackHandler);
+ }, [webRTC]); // 只依赖webRTC,移除handleRemoteStream依赖
// 获取桌面共享流
const getDesktopStream = useCallback(async (): Promise => {
@@ -241,10 +258,10 @@ export function useDesktopShareBusiness() {
return data.code;
}, []);
- // 创建房间(只建立连接,等待对方加入)
- const createRoom = useCallback(async (): Promise => {
+ // 创建房间并立即开始桌面共享
+ const createRoomAndStartSharing = useCallback(async (): Promise => {
try {
- updateState({ error: null, isWaitingForPeer: false });
+ setState(prev => ({ ...prev, error: null, isWaitingForPeer: false }));
// 从后端获取房间代码
const roomCode = await createRoomFromBackend();
@@ -255,57 +272,120 @@ export function useDesktopShareBusiness() {
await webRTC.connect(roomCode, 'sender');
console.log('[DesktopShare] ✅ WebSocket连接已建立');
- updateState({
+ setState(prev => ({
+ ...prev,
+ connectionCode: roomCode,
+ }));
+
+ // 立即开始桌面共享(不等待P2P连接)
+ console.log('[DesktopShare] 📺 正在请求桌面共享权限...');
+ const stream = await getDesktopStream();
+
+ // 停止之前的流(如果有)
+ if (localStreamRef.current) {
+ localStreamRef.current.getTracks().forEach(track => track.stop());
+ }
+
+ localStreamRef.current = stream;
+ console.log('[DesktopShare] ✅ 桌面流获取成功,流ID:', stream.id, '轨道数:', stream.getTracks().length);
+
+ // 确保状态更新能正确触发重新渲染
+ setState(prev => ({
+ ...prev,
+ isSharing: true,
+ isWaitingForPeer: true, // 等待观看者加入
+ localStream: stream, // 更新状态以触发组件重新渲染
+ }));
+
+ // 再次确认状态已更新(用于调试)
+ console.log('[DesktopShare] 🎉 桌面共享已开始,状态已更新,等待观看者加入');
+ return roomCode;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '创建房间失败';
+ console.error('[DesktopShare] ❌ 创建房间失败:', error);
+ setState(prev => ({ ...prev, error: errorMessage, connectionCode: '', isWaitingForPeer: false, isSharing: false }));
+ throw error;
+ }
+ }, [webRTC, createRoomFromBackend, getDesktopStream]); // 移除updateState依赖
+
+ // 创建房间(保留原有方法以兼容性)
+ const createRoom = useCallback(async (): Promise => {
+ try {
+ setState(prev => ({ ...prev, error: null, isWaitingForPeer: false }));
+
+ // 从后端获取房间代码
+ const roomCode = await createRoomFromBackend();
+ console.log('[DesktopShare] 🚀 创建桌面共享房间,代码:', roomCode);
+
+ // 建立WebRTC连接(作为发送方)
+ console.log('[DesktopShare] 📡 正在建立WebRTC连接...');
+ await webRTC.connect(roomCode, 'sender');
+ console.log('[DesktopShare] ✅ WebSocket连接已建立');
+
+ setState(prev => ({
+ ...prev,
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 });
+ setState(prev => ({ ...prev, error: errorMessage, connectionCode: '', isWaitingForPeer: false }));
throw error;
}
- }, [webRTC, createRoomFromBackend, updateState]);
+ }, [webRTC, createRoomFromBackend]); // 移除updateState依赖
- // 开始桌面共享(在接收方加入后)
+ // 开始桌面共享(支持有或无P2P连接状态)
const startSharing = useCallback(async (): Promise => {
try {
- // 检查P2P连接状态(与switchDesktop保持一致)
- if (!webRTC.getConnectState().isPeerConnected) {
- throw new Error('P2P连接未建立');
- }
-
- updateState({ error: null });
+ setState(prev => ({ ...prev, error: null }));
console.log('[DesktopShare] 📺 正在请求桌面共享权限...');
// 获取桌面流
const stream = await getDesktopStream();
- // 停止之前的流(如果有)- 与switchDesktop保持一致
+ // 停止之前的流(如果有)
if (localStreamRef.current) {
localStreamRef.current.getTracks().forEach(track => track.stop());
}
localStreamRef.current = stream;
- console.log('[DesktopShare] ✅ 桌面流获取成功');
+ console.log('[DesktopShare] ✅ 桌面流获取成功,流ID:', stream.id, '轨道数:', stream.getTracks().length);
- // 设置新的视频发送 - 与switchDesktop保持一致
- await setupVideoSending(stream);
- console.log('[DesktopShare] ✅ 桌面共享开始完成');
-
- updateState({
- isSharing: true,
- isWaitingForPeer: false,
- });
+ // 如果P2P连接已建立,立即设置视频发送
+ if (webRTC.getConnectState().isPeerConnected) {
+ await setupVideoSending(stream);
+ console.log('[DesktopShare] ✅ 桌面共享开始完成(P2P已连接)');
+ setState(prev => ({
+ ...prev,
+ isSharing: true,
+ isWaitingForPeer: false,
+ localStream: stream,
+ }));
+ } else {
+ // P2P连接未建立,等待观看者加入
+ console.log('[DesktopShare] 📱 桌面流已准备,等待观看者加入建立P2P连接');
+ setState(prev => ({
+ ...prev,
+ isSharing: true,
+ isWaitingForPeer: true,
+ localStream: stream,
+ }));
+ }
console.log('[DesktopShare] 🎉 桌面共享已开始');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '开始桌面共享失败';
console.error('[DesktopShare] ❌ 开始共享失败:', error);
- updateState({ error: errorMessage, isSharing: false });
+ setState(prev => ({
+ ...prev,
+ error: errorMessage,
+ isSharing: false,
+ localStream: null,
+ }));
// 清理资源
if (localStreamRef.current) {
@@ -315,20 +395,16 @@ export function useDesktopShareBusiness() {
throw error;
}
- }, [webRTC, getDesktopStream, setupVideoSending, updateState]);
+ }, [webRTC, getDesktopStream]); // 移除setupVideoSending和updateState依赖
// 切换桌面共享(重新选择屏幕)
const switchDesktop = useCallback(async (): Promise => {
try {
- if (!webRTC.getConnectState().isPeerConnected) {
- throw new Error('P2P连接未建立');
- }
-
if (!state.isSharing) {
throw new Error('当前未在共享桌面');
}
- updateState({ error: null });
+ setState(prev => ({ ...prev, error: null }));
console.log('[DesktopShare] 🔄 正在切换桌面共享...');
// 获取新的桌面流
@@ -340,19 +416,26 @@ export function useDesktopShareBusiness() {
}
localStreamRef.current = newStream;
- console.log('[DesktopShare] ✅ 新桌面流获取成功');
+ console.log('[DesktopShare] ✅ 新桌面流获取成功,流ID:', newStream.id, '轨道数:', newStream.getTracks().length);
- // 设置新的视频发送
- await setupVideoSending(newStream);
- console.log('[DesktopShare] ✅ 桌面切换完成');
+ // 更新状态中的本地流
+ setState(prev => ({ ...prev, localStream: newStream }));
+
+ // 如果有P2P连接,设置新的视频发送
+ if (webRTC.getConnectState().isPeerConnected) {
+ await setupVideoSending(newStream);
+ console.log('[DesktopShare] ✅ 桌面切换完成(已推流给观看者)');
+ } else {
+ console.log('[DesktopShare] ✅ 桌面切换完成(等待观看者加入)');
+ }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '切换桌面失败';
console.error('[DesktopShare] ❌ 切换桌面失败:', error);
- updateState({ error: errorMessage });
+ setState(prev => ({ ...prev, error: errorMessage }));
throw error;
}
- }, [webRTC, state.isSharing, getDesktopStream, setupVideoSending, updateState]);
+ }, [webRTC, state.isSharing, getDesktopStream]); // 移除setupVideoSending和updateState依赖
// 停止桌面共享
const stopSharing = useCallback(async (): Promise => {
@@ -377,20 +460,22 @@ export function useDesktopShareBusiness() {
// 断开WebRTC连接
webRTC.disconnect();
- updateState({
+ setState(prev => ({
+ ...prev,
isSharing: false,
connectionCode: '',
error: null,
isWaitingForPeer: false,
- });
+ localStream: null,
+ }));
console.log('[DesktopShare] 桌面共享已停止');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '停止桌面共享失败';
console.error('[DesktopShare] 停止共享失败:', error);
- updateState({ error: errorMessage });
+ setState(prev => ({ ...prev, error: errorMessage }));
}
- }, [webRTC, updateState]);
+ }, [webRTC]); // 移除updateState依赖,直接使用setState
// 重置桌面共享到初始状态(让用户重新选择桌面)
const resetSharing = useCallback(async (): Promise => {
@@ -413,24 +498,85 @@ export function useDesktopShareBusiness() {
}
// 保留WebSocket连接和房间代码,但重置共享状态
- updateState({
+ setState(prev => ({
+ ...prev,
isSharing: false,
error: null,
isWaitingForPeer: false,
- });
+ localStream: null,
+ }));
console.log('[DesktopShare] 桌面共享已重置到初始状态');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '重置桌面共享失败';
console.error('[DesktopShare] 重置共享失败:', error);
+ setState(prev => ({ ...prev, error: errorMessage }));
+ }
+ }, [webRTC]); // 移除updateState依赖
+
+ // 处理P2P连接断开但保持桌面共享状态(用于接收方离开房间的情况)
+ const handlePeerDisconnect = useCallback(async (): Promise => {
+ try {
+ console.log('[DesktopShare] P2P连接断开,但保持桌面共享状态以便新用户加入');
+
+ // 移除当前的发送器(清理P2P连接相关资源)
+ if (currentSenderRef.current) {
+ webRTC.removeTrack(currentSenderRef.current);
+ currentSenderRef.current = null;
+ }
+
+ // 保持本地流和共享状态,只设置为等待新的对等方
+ setState(prev => ({
+ ...prev,
+ isWaitingForPeer: true,
+ error: null,
+ }));
+
+ console.log('[DesktopShare] 已清理P2P连接资源,等待新用户加入');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '处理P2P断开失败';
+ console.error('[DesktopShare] 处理P2P断开失败:', error);
+ setState(prev => ({ ...prev, error: errorMessage }));
+ }
+ }, [webRTC]); // 移除updateState依赖
+
+ // 重新建立P2P连接并推流(用于新用户加入房间的情况)
+ const resumeSharing = useCallback(async (): Promise => {
+ try {
+ console.log('[DesktopShare] 新用户加入,重新建立P2P连接并推流');
+
+ // 检查是否还在共享状态且有本地流
+ if (!state.isSharing || !localStreamRef.current) {
+ console.log('[DesktopShare] 当前没有在共享或没有本地流,无法恢复推流');
+ return;
+ }
+
+ // 检查P2P连接状态
+ if (!webRTC.getConnectState().isPeerConnected) {
+ console.log('[DesktopShare] P2P连接未建立,等待连接完成');
+ return;
+ }
+
+ // 重新设置视频发送
+ await setupVideoSending(localStreamRef.current);
+
+ updateState({
+ isWaitingForPeer: false,
+ error: null,
+ });
+
+ console.log('[DesktopShare] ✅ P2P连接已恢复,桌面共享流已重新建立');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '恢复桌面共享失败';
+ console.error('[DesktopShare] 恢复桌面共享失败:', error);
updateState({ error: errorMessage });
}
- }, [webRTC, updateState]);
+ }, [webRTC, state.isSharing]); // 移除setupVideoSending和updateState依赖
// 加入桌面共享观看
const joinSharing = useCallback(async (code: string): Promise => {
try {
- updateState({ error: null });
+ setState(prev => ({ ...prev, error: null }));
console.log('[DesktopShare] 🔍 正在加入桌面共享观看:', code);
// 连接WebRTC
@@ -452,7 +598,7 @@ export function useDesktopShareBusiness() {
});
}
- updateState({ isViewing: true });
+ setState(prev => ({ ...prev, isViewing: true }));
console.log('[DesktopShare] 👁️ 已进入桌面共享观看模式,等待接收流...');
// 设置一个超时检查,如果长时间没有收到流,输出警告
@@ -465,10 +611,10 @@ export function useDesktopShareBusiness() {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '加入桌面共享失败';
console.error('[DesktopShare] ❌ 加入观看失败:', error);
- updateState({ error: errorMessage, isViewing: false });
+ setState(prev => ({ ...prev, error: errorMessage, isViewing: false }));
throw error;
}
- }, [webRTC, updateState, state.remoteStream]);
+ }, [webRTC, state.remoteStream]); // 移除updateState依赖
// 停止观看桌面共享
const stopViewing = useCallback(async (): Promise => {
@@ -478,19 +624,20 @@ export function useDesktopShareBusiness() {
// 断开WebRTC连接
webRTC.disconnect();
- updateState({
+ setState(prev => ({
+ ...prev,
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 });
+ setState(prev => ({ ...prev, error: errorMessage }));
}
- }, [webRTC, updateState]);
+ }, [webRTC]); // 移除updateState依赖
// 设置远程视频元素引用
const setRemoteVideoRef = useCallback((videoElement: HTMLVideoElement | null) => {
@@ -500,6 +647,55 @@ export function useDesktopShareBusiness() {
}
}, [state.remoteStream]);
+ // 监听P2P连接状态变化,自动处理重新连接
+ const prevPeerConnectedForResumeRef = useRef(false);
+
+ useEffect(() => {
+ const isPeerConnected = webRTC.getConnectState().isPeerConnected;
+ const wasPreviouslyDisconnected = !prevPeerConnectedForResumeRef.current;
+
+ // 更新ref
+ prevPeerConnectedForResumeRef.current = isPeerConnected;
+
+ // 当P2P连接从断开变为连接且正在等待对方时,自动恢复推流
+ if (isPeerConnected &&
+ wasPreviouslyDisconnected &&
+ state.isWaitingForPeer &&
+ state.isSharing) {
+ console.log('[DesktopShare] 🔄 P2P连接已建立,自动恢复桌面共享推流');
+
+ // 调用resumeSharing但不依赖它
+ const handleResume = async () => {
+ try {
+ console.log('[DesktopShare] 新用户加入,重新建立P2P连接并推流');
+
+ // 检查是否还在共享状态且有本地流
+ if (!state.isSharing || !localStreamRef.current) {
+ console.log('[DesktopShare] 当前没有在共享或没有本地流,无法恢复推流');
+ return;
+ }
+
+ // 重新设置视频发送
+ await setupVideoSending(localStreamRef.current);
+
+ setState(prev => ({
+ ...prev,
+ isWaitingForPeer: false,
+ error: null,
+ }));
+
+ console.log('[DesktopShare] ✅ P2P连接已恢复,桌面共享流已重新建立');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '恢复桌面共享失败';
+ console.error('[DesktopShare] 恢复桌面共享失败:', error);
+ setState(prev => ({ ...prev, error: errorMessage }));
+ }
+ };
+
+ handleResume();
+ }
+ }, [webRTC.getConnectState().isPeerConnected, state.isWaitingForPeer, state.isSharing]); // 移除resumeSharing依赖
+
// 清理资源
useEffect(() => {
return () => {
@@ -515,6 +711,7 @@ export function useDesktopShareBusiness() {
isViewing: state.isViewing,
connectionCode: state.connectionCode,
remoteStream: state.remoteStream,
+ localStream: state.localStream, // 使用状态中的流
error: state.error,
isWaitingForPeer: state.isWaitingForPeer,
isConnected: webRTC.getConnectState().isConnected,
@@ -526,10 +723,13 @@ export function useDesktopShareBusiness() {
// 方法
createRoom, // 创建房间
+ createRoomAndStartSharing, // 创建房间并立即开始桌面共享
startSharing, // 选择桌面并建立P2P连接
switchDesktop, // 新增:切换桌面
stopSharing,
resetSharing, // 重置到初始状态,保留房间连接
+ handlePeerDisconnect, // 处理P2P断开但保持桌面共享
+ resumeSharing, // 重新建立P2P连接并推流
joinSharing,
stopViewing,
setRemoteVideoRef,
diff --git a/chuan-next/src/lib/config.ts b/chuan-next/src/lib/config.ts
index 10d1fdb..81b94dc 100644
--- a/chuan-next/src/lib/config.ts
+++ b/chuan-next/src/lib/config.ts
@@ -26,16 +26,17 @@ const getCurrentBaseUrl = () => {
// 动态获取 WebSocket URL - 总是在客户端运行时计算
const getCurrentWsUrl = () => {
+ return "ws://192.168.1.120:8080"
if (typeof window !== 'undefined') {
// 检查是否是 Next.js 开发服务器(端口 3000 或 3001)
- const isNextDevServer = window.location.hostname === 'localhost' &&
- (window.location.port === '3000' || window.location.port === '3001');
-
+ const isNextDevServer = window.location.hostname === 'localhost' &&
+ (window.location.port === '3000' || window.location.port === '3001');
+
if (isNextDevServer) {
// 开发模式:通过 Next.js 开发服务器访问,连接到后端 WebSocket
return 'ws://localhost:8080';
}
-
+
// 生产模式或通过 Go 服务器访问:使用当前域名和端口
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${window.location.host}`;
@@ -49,28 +50,28 @@ export const config = {
isDev: getEnv('NODE_ENV') === 'development',
isProd: getEnv('NODE_ENV') === 'production',
isStatic: typeof window !== 'undefined', // 客户端运行时认为是静态模式
-
+
// API配置
api: {
// 后端API地址 (服务器端使用)
backendUrl: getEnv('GO_BACKEND_URL', 'http://localhost:8080'),
-
+
// 前端API基础URL (客户端使用) - 开发模式下调用 Next.js API 路由
baseUrl: getEnv('NEXT_PUBLIC_API_BASE_URL', 'http://localhost:3000'),
-
+
// 直接后端URL (客户端在静态模式下使用) - 如果环境变量为空,则使用当前域名
directBackendUrl: getEnv('NEXT_PUBLIC_BACKEND_URL') || getCurrentBaseUrl(),
-
+
// WebSocket地址 - 在客户端运行时动态计算,不在构建时预设
wsUrl: '', // 将通过 getWsUrl() 函数动态获取
},
-
+
// 超时配置
timeout: {
api: 30000, // 30秒
ws: 60000, // 60秒
},
-
+
// 重试配置
retry: {
max: 3,
@@ -122,12 +123,12 @@ export function getWsUrl(): string {
if (envWsUrl) {
return envWsUrl;
}
-
+
// 如果是服务器端(SSG构建时),返回空字符串
if (typeof window === 'undefined') {
return '';
}
-
+
// 客户端运行时动态计算
return getCurrentWsUrl();
}
diff --git a/cmd/config.go b/cmd/config.go
index 1d06858..9671343 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -14,6 +14,16 @@ import (
type Config struct {
Port int
FrontendDir string
+ TurnConfig TurnConfig
+}
+
+// TurnConfig TURN服务器配置
+type TurnConfig struct {
+ Enabled bool `json:"enabled"`
+ Port int `json:"port"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+ Realm string `json:"realm"`
}
// loadEnvFile 加载环境变量文件
@@ -64,6 +74,11 @@ func showHelp() {
fmt.Println(" 环境变量:")
fmt.Println(" PORT=8080 - 服务器监听端口")
fmt.Println(" FRONTEND_DIR=/path - 外部前端文件目录 (可选)")
+ fmt.Println(" TURN_ENABLED=true - 启用TURN服务器")
+ fmt.Println(" TURN_PORT=3478 - TURN服务器端口")
+ fmt.Println(" TURN_USERNAME=user - TURN服务器用户名")
+ fmt.Println(" TURN_PASSWORD=pass - TURN服务器密码")
+ fmt.Println(" TURN_REALM=localhost - TURN服务器域")
fmt.Println(" 命令行参数:")
flag.PrintDefaults()
fmt.Println("")
@@ -73,6 +88,7 @@ func showHelp() {
fmt.Println(" ./file-transfer-server")
fmt.Println(" ./file-transfer-server -port 3000")
fmt.Println(" PORT=8080 FRONTEND_DIR=./dist ./file-transfer-server")
+ fmt.Println(" TURN_ENABLED=true TURN_PORT=3478 ./file-transfer-server")
}
// loadConfig 加载应用配置
@@ -90,6 +106,27 @@ func loadConfig() *Config {
}
}
+ // TURN 配置默认值
+ turnEnabled := os.Getenv("TURN_ENABLED") == "true"
+ turnPort := 3478
+ if envTurnPort := os.Getenv("TURN_PORT"); envTurnPort != "" {
+ if port, err := strconv.Atoi(envTurnPort); err == nil {
+ turnPort = port
+ }
+ }
+ turnUsername := os.Getenv("TURN_USERNAME")
+ if turnUsername == "" {
+ turnUsername = "chuan"
+ }
+ turnPassword := os.Getenv("TURN_PASSWORD")
+ if turnPassword == "" {
+ turnPassword = "chuan123"
+ }
+ turnRealm := os.Getenv("TURN_REALM")
+ if turnRealm == "" {
+ turnRealm = "localhost"
+ }
+
// 定义命令行参数
var port = flag.Int("port", defaultPort, "服务器监听端口 (可通过 PORT 环境变量设置)")
var help = flag.Bool("help", false, "显示帮助信息")
@@ -104,6 +141,13 @@ func loadConfig() *Config {
config := &Config{
Port: *port,
FrontendDir: os.Getenv("FRONTEND_DIR"),
+ TurnConfig: TurnConfig{
+ Enabled: turnEnabled,
+ Port: turnPort,
+ Username: turnUsername,
+ Password: turnPassword,
+ Realm: turnRealm,
+ },
}
return config
@@ -121,4 +165,14 @@ func logConfig(config *Config) {
} else {
log.Printf("📦 使用内嵌前端文件")
}
+
+ // 记录 TURN 配置信息
+ if config.TurnConfig.Enabled {
+ log.Printf("🔄 TURN服务器已启用")
+ log.Printf(" 端口: %d", config.TurnConfig.Port)
+ log.Printf(" 用户名: %s", config.TurnConfig.Username)
+ log.Printf(" 域: %s", config.TurnConfig.Realm)
+ } else {
+ log.Printf("❌ TURN服务器已禁用")
+ }
}
diff --git a/cmd/main.go b/cmd/main.go
index 421af20..e51b7bd 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -18,8 +18,8 @@ func main() {
logConfig(config)
// 设置路由
- router := setupRouter()
+ routerSetup := setupRouter(config)
// 运行服务器(包含启动和优雅关闭)
- RunServer(config, router)
+ RunServer(config, routerSetup)
}
diff --git a/cmd/router.go b/cmd/router.go
index 82d2e29..125cc00 100644
--- a/cmd/router.go
+++ b/cmd/router.go
@@ -11,8 +11,14 @@ import (
"github.com/go-chi/cors"
)
+// RouterSetup 路由设置结果
+type RouterSetup struct {
+ Handler *handlers.Handler
+ Router http.Handler
+}
+
// setupRouter 设置路由和中间件
-func setupRouter() http.Handler {
+func setupRouter(config *Config) *RouterSetup {
// 初始化处理器
h := handlers.NewHandler()
@@ -22,12 +28,15 @@ func setupRouter() http.Handler {
setupMiddleware(router)
// 设置API路由
- setupAPIRoutes(router, h)
+ setupAPIRoutes(router, h, config)
// 设置前端路由
router.Handle("/*", web.CreateFrontendHandler())
- return router
+ return &RouterSetup{
+ Handler: h,
+ Router: router,
+ }
}
// setupMiddleware 设置中间件
@@ -48,11 +57,20 @@ func setupMiddleware(r *chi.Mux) {
}
// setupAPIRoutes 设置API路由
-func setupAPIRoutes(r *chi.Mux, h *handlers.Handler) {
+func setupAPIRoutes(r *chi.Mux, h *handlers.Handler, config *Config) {
// WebRTC信令WebSocket路由
r.Get("/api/ws/webrtc", h.HandleWebRTCWebSocket)
// WebRTC房间API
r.Post("/api/create-room", h.CreateRoomHandler)
r.Get("/api/room-info", h.WebRTCRoomStatusHandler)
+
+ // TURN服务器API(仅在启用时可用)
+ if config.TurnConfig.Enabled {
+ r.Get("/api/turn/stats", h.TurnStatsHandler)
+ r.Get("/api/turn/config", h.TurnConfigHandler)
+ }
+
+ // 管理API
+ r.Get("/api/admin/status", h.AdminStatusHandler)
}
diff --git a/cmd/server.go b/cmd/server.go
index 409ef13..8f9a4ab 100644
--- a/cmd/server.go
+++ b/cmd/server.go
@@ -9,30 +9,56 @@ import (
"os/signal"
"syscall"
"time"
+
+ "chuan/internal/services"
)
// Server 服务器结构
type Server struct {
- httpServer *http.Server
- config *Config
+ httpServer *http.Server
+ config *Config
+ turnService *services.TurnService
}
// NewServer 创建新的服务器实例
-func NewServer(config *Config, handler http.Handler) *Server {
- return &Server{
+func NewServer(config *Config, routerSetup *RouterSetup) *Server {
+ server := &Server{
httpServer: &http.Server{
Addr: fmt.Sprintf(":%d", config.Port),
- Handler: handler,
+ Handler: routerSetup.Router,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
},
config: config,
}
+
+ // 如果启用了TURN服务器,创建TURN服务实例
+ if config.TurnConfig.Enabled {
+ turnConfig := services.TurnServiceConfig{
+ Port: config.TurnConfig.Port,
+ Username: config.TurnConfig.Username,
+ Password: config.TurnConfig.Password,
+ Realm: config.TurnConfig.Realm,
+ }
+ server.turnService = services.NewTurnService(turnConfig)
+
+ // 将TURN服务设置到处理器中
+ routerSetup.Handler.SetTurnService(server.turnService)
+ }
+
+ return server
}
// Start 启动服务器
func (s *Server) Start() error {
+ // 启动TURN服务器(如果启用)
+ if s.turnService != nil {
+ if err := s.turnService.Start(); err != nil {
+ return fmt.Errorf("启动TURN服务器失败: %v", err)
+ }
+ }
+
log.Printf("🚀 服务器启动在端口 :%d", s.config.Port)
return s.httpServer.ListenAndServe()
}
@@ -40,6 +66,14 @@ func (s *Server) Start() error {
// Stop 停止服务器
func (s *Server) Stop(ctx context.Context) error {
log.Println("🛑 正在关闭服务器...")
+
+ // 停止TURN服务器(如果启用)
+ if s.turnService != nil {
+ if err := s.turnService.Stop(); err != nil {
+ log.Printf("⚠️ 停止TURN服务器失败: %v", err)
+ }
+ }
+
return s.httpServer.Shutdown(ctx)
}
@@ -62,8 +96,8 @@ func (s *Server) WaitForShutdown() {
}
// RunServer 运行服务器(包含启动和优雅关闭)
-func RunServer(config *Config, handler http.Handler) {
- server := NewServer(config, handler)
+func RunServer(config *Config, routerSetup *RouterSetup) {
+ server := NewServer(config, routerSetup)
// 启动服务器
go func() {
diff --git a/go.mod b/go.mod
index f11aae2..7335fd4 100644
--- a/go.mod
+++ b/go.mod
@@ -7,3 +7,15 @@ require (
github.com/go-chi/cors v1.2.1
github.com/gorilla/websocket v1.5.3
)
+
+require (
+ github.com/pion/dtls/v2 v2.2.7 // indirect
+ github.com/pion/logging v0.2.2 // indirect
+ github.com/pion/randutil v0.1.0 // indirect
+ github.com/pion/stun/v2 v2.0.0 // indirect
+ github.com/pion/transport/v2 v2.2.1 // indirect
+ github.com/pion/transport/v3 v3.0.2 // indirect
+ github.com/pion/turn/v3 v3.0.3 // indirect
+ golang.org/x/crypto v0.21.0 // indirect
+ golang.org/x/sys v0.18.0 // indirect
+)
diff --git a/go.sum b/go.sum
index a07d7b1..f6126f5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,90 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
+github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
+github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
+github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
+github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
+github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
+github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
+github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
+github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
+github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
+github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
+github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
+github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
+github.com/pion/turn/v3 v3.0.3 h1:1e3GVk8gHZLPBA5LqadWYV60lmaKUaHCkm9DX9CkGcE=
+github.com/pion/turn/v3 v3.0.3/go.mod h1:vw0Dz420q7VYAF3J4wJKzReLHIo2LGp4ev8nXQexYsc=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index b2b8faa..f110b6f 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -10,6 +10,7 @@ import (
type Handler struct {
webrtcService *services.WebRTCService
+ turnService *services.TurnService
}
func NewHandler() *Handler {
@@ -18,6 +19,11 @@ func NewHandler() *Handler {
}
}
+// SetTurnService 设置TURN服务实例
+func (h *Handler) SetTurnService(turnService *services.TurnService) {
+ h.turnService = turnService
+}
+
// HandleWebRTCWebSocket 处理WebRTC信令WebSocket连接
func (h *Handler) HandleWebRTCWebSocket(w http.ResponseWriter, r *http.Request) {
h.webrtcService.HandleWebSocket(w, r)
@@ -105,3 +111,101 @@ func (h *Handler) GetRoomStatusHandler(w http.ResponseWriter, r *http.Request) {
status := h.webrtcService.GetRoomStatus(code)
json.NewEncoder(w).Encode(status)
}
+
+// TurnStatsHandler 获取TURN服务器统计信息API
+func (h *Handler) TurnStatsHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+
+ if r.Method != http.MethodGet {
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "方法不允许",
+ })
+ return
+ }
+
+ if h.turnService == nil {
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "TURN服务器未启用",
+ })
+ return
+ }
+
+ stats := h.turnService.GetStats()
+ response := map[string]interface{}{
+ "success": true,
+ "data": stats,
+ }
+
+ json.NewEncoder(w).Encode(response)
+}
+
+// TurnConfigHandler 获取TURN服务器配置信息API(用于前端WebRTC配置)
+func (h *Handler) TurnConfigHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+
+ if r.Method != http.MethodGet {
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "方法不允许",
+ })
+ return
+ }
+
+ if h.turnService == nil || !h.turnService.IsRunning() {
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "TURN服务器未启用或未运行",
+ })
+ return
+ }
+
+ turnInfo := h.turnService.GetTurnServerInfo()
+ response := map[string]interface{}{
+ "success": true,
+ "data": turnInfo,
+ }
+
+ json.NewEncoder(w).Encode(response)
+}
+
+// AdminStatusHandler 获取服务器总体状态API
+func (h *Handler) AdminStatusHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+
+ if r.Method != http.MethodGet {
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "success": false,
+ "message": "方法不允许",
+ })
+ return
+ }
+
+ // 获取WebRTC服务状态
+ // 这里简化,实际可以从WebRTC服务获取更多信息
+ webrtcStatus := map[string]interface{}{
+ "isRunning": true, // WebRTC服务总是运行的
+ }
+
+ // 获取TURN服务状态
+ var turnStatus interface{}
+ if h.turnService != nil {
+ turnStatus = h.turnService.GetStats()
+ } else {
+ turnStatus = map[string]interface{}{
+ "isRunning": false,
+ "message": "TURN服务器未启用",
+ }
+ }
+
+ response := map[string]interface{}{
+ "success": true,
+ "data": map[string]interface{}{
+ "webrtc": webrtcStatus,
+ "turn": turnStatus,
+ },
+ }
+
+ json.NewEncoder(w).Encode(response)
+}
diff --git a/internal/services/turn_service.go b/internal/services/turn_service.go
new file mode 100644
index 0000000..86f82ff
--- /dev/null
+++ b/internal/services/turn_service.go
@@ -0,0 +1,234 @@
+package services
+
+import (
+ "fmt"
+ "log"
+ "net"
+ "sync"
+
+ "github.com/pion/turn/v3"
+)
+
+// TurnService TURN服务器结构
+type TurnService struct {
+ server *turn.Server
+ config TurnServiceConfig
+ stats *TurnStats
+ isRunning bool
+ mu sync.RWMutex
+}
+
+// TurnServiceConfig TURN服务器配置
+type TurnServiceConfig struct {
+ Port int
+ Username string
+ Password string
+ Realm string
+}
+
+// TurnStats TURN服务器统计信息
+type TurnStats struct {
+ ActiveAllocations int64
+ TotalAllocations int64
+ BytesTransferred int64
+ PacketsTransferred int64
+ Connections int64
+ mu sync.RWMutex
+}
+
+// NewTurnService 创建新的TURN服务实例
+func NewTurnService(config TurnServiceConfig) *TurnService {
+ return &TurnService{
+ config: config,
+ stats: &TurnStats{},
+ }
+}
+
+// Start 启动TURN服务器
+func (ts *TurnService) Start() error {
+ ts.mu.Lock()
+ defer ts.mu.Unlock()
+
+ if ts.isRunning {
+ return fmt.Errorf("TURN服务器已在运行")
+ }
+
+ // 监听UDP端口
+ udpListener, err := net.ListenPacket("udp4", fmt.Sprintf("0.0.0.0:%d", ts.config.Port))
+ if err != nil {
+ return fmt.Errorf("无法监听UDP端口: %v", err)
+ }
+
+ // 监听TCP端口
+ tcpListener, err := net.Listen("tcp4", fmt.Sprintf("0.0.0.0:%d", ts.config.Port))
+ if err != nil {
+ udpListener.Close()
+ return fmt.Errorf("无法监听TCP端口: %v", err)
+ }
+
+ // 创建TURN服务器配置
+ turnConfig := turn.ServerConfig{
+ Realm: ts.config.Realm,
+ AuthHandler: ts.authHandler,
+ PacketConnConfigs: []turn.PacketConnConfig{
+ {
+ PacketConn: udpListener,
+ RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
+ RelayAddress: net.ParseIP("127.0.0.1"), // 在生产环境中应该使用公网IP
+ Address: "0.0.0.0",
+ },
+ },
+ },
+ ListenerConfigs: []turn.ListenerConfig{
+ {
+ Listener: tcpListener,
+ RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
+ RelayAddress: net.ParseIP("127.0.0.1"), // 在生产环境中应该使用公网IP
+ Address: "0.0.0.0",
+ },
+ },
+ },
+ }
+
+ // 创建TURN服务器
+ server, err := turn.NewServer(turnConfig)
+ if err != nil {
+ udpListener.Close()
+ tcpListener.Close()
+ return fmt.Errorf("创建TURN服务器失败: %v", err)
+ }
+
+ ts.server = server
+ ts.isRunning = true
+
+ log.Printf("🔄 TURN服务器启动成功,监听端口: %d", ts.config.Port)
+ log.Printf(" 用户名: %s, 域: %s", ts.config.Username, ts.config.Realm)
+
+ return nil
+}
+
+// Stop 停止TURN服务器
+func (ts *TurnService) Stop() error {
+ ts.mu.Lock()
+ defer ts.mu.Unlock()
+
+ if !ts.isRunning {
+ return fmt.Errorf("TURN服务器未运行")
+ }
+
+ if ts.server != nil {
+ if err := ts.server.Close(); err != nil {
+ return fmt.Errorf("关闭TURN服务器失败: %v", err)
+ }
+ }
+
+ ts.isRunning = false
+ log.Printf("🛑 TURN服务器已停止")
+
+ return nil
+}
+
+// IsRunning 检查TURN服务器是否正在运行
+func (ts *TurnService) IsRunning() bool {
+ ts.mu.RLock()
+ defer ts.mu.RUnlock()
+ return ts.isRunning
+}
+
+// authHandler 认证处理器
+func (ts *TurnService) authHandler(username string, realm string, srcAddr net.Addr) ([]byte, bool) {
+ // 记录连接统计
+ ts.stats.mu.Lock()
+ ts.stats.Connections++
+ ts.stats.mu.Unlock()
+
+ log.Printf("🔐 TURN认证请求: 用户=%s, 域=%s, 地址=%s", username, realm, srcAddr.String())
+
+ // 简单的用户名密码验证
+ if username == ts.config.Username && realm == ts.config.Realm {
+ // 记录分配统计
+ ts.stats.mu.Lock()
+ ts.stats.ActiveAllocations++
+ ts.stats.TotalAllocations++
+ ts.stats.mu.Unlock()
+
+ log.Printf("📊 TURN认证成功: 活跃分配=%d, 总分配=%d", ts.stats.ActiveAllocations, ts.stats.TotalAllocations)
+
+ // 返回密码的key
+ return turn.GenerateAuthKey(username, ts.config.Realm, ts.config.Password), true
+ }
+
+ log.Printf("❌ TURN认证失败: 用户=%s", username)
+ return nil, false
+}
+
+// GetStats 获取统计信息
+func (ts *TurnService) GetStats() TurnStatsResponse {
+ ts.stats.mu.RLock()
+ defer ts.stats.mu.RUnlock()
+
+ return TurnStatsResponse{
+ IsRunning: ts.IsRunning(),
+ ActiveAllocations: ts.stats.ActiveAllocations,
+ TotalAllocations: ts.stats.TotalAllocations,
+ BytesTransferred: ts.stats.BytesTransferred,
+ PacketsTransferred: ts.stats.PacketsTransferred,
+ Connections: ts.stats.Connections,
+ Port: ts.config.Port,
+ Username: ts.config.Username,
+ Realm: ts.config.Realm,
+ }
+}
+
+// GetTurnServerInfo 获取TURN服务器信息用于客户端
+func (ts *TurnService) GetTurnServerInfo() TurnServerInfo {
+ if !ts.IsRunning() {
+ return TurnServerInfo{}
+ }
+
+ return TurnServerInfo{
+ URLs: []string{fmt.Sprintf("turn:localhost:%d", ts.config.Port)},
+ Username: ts.config.Username,
+ Credential: ts.config.Password,
+ }
+}
+
+// UpdateStats 更新传输统计 (可以从外部调用)
+func (ts *TurnService) UpdateStats(bytes, packets int64) {
+ ts.stats.mu.Lock()
+ defer ts.stats.mu.Unlock()
+
+ ts.stats.BytesTransferred += bytes
+ ts.stats.PacketsTransferred += packets
+}
+
+// DecrementActiveAllocations 减少活跃分配数(当连接关闭时调用)
+func (ts *TurnService) DecrementActiveAllocations() {
+ ts.stats.mu.Lock()
+ defer ts.stats.mu.Unlock()
+
+ if ts.stats.ActiveAllocations > 0 {
+ ts.stats.ActiveAllocations--
+ log.Printf("📊 TURN分配释放: 活跃分配=%d", ts.stats.ActiveAllocations)
+ }
+}
+
+// TurnStatsResponse TURN统计响应结构
+type TurnStatsResponse struct {
+ IsRunning bool `json:"isRunning"`
+ ActiveAllocations int64 `json:"activeAllocations"`
+ TotalAllocations int64 `json:"totalAllocations"`
+ BytesTransferred int64 `json:"bytesTransferred"`
+ PacketsTransferred int64 `json:"packetsTransferred"`
+ Connections int64 `json:"connections"`
+ Port int `json:"port"`
+ Username string `json:"username"`
+ Realm string `json:"realm"`
+}
+
+// TurnServerInfo TURN服务器信息结构 (用于WebRTC配置)
+type TurnServerInfo struct {
+ URLs []string `json:"urls"`
+ Username string `json:"username"`
+ Credential string `json:"credential"`
+}
\ No newline at end of file