= ({ onRestart, o
value={textInput}
onChange={handleTextInputChange}
onPaste={handlePaste}
- placeholder="在这里编辑文字内容...
💡 支持实时同步编辑,对方可以看到你的修改
💡 可以直接粘贴图片 (Ctrl+V)"
- className="w-full h-40 px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-slate-700 placeholder-slate-400"
+ disabled={!connection.isPeerConnected}
+ placeholder={connection.isPeerConnected
+ ? "在这里编辑文字内容...
💡 支持实时同步编辑,对方可以看到你的修改
💡 可以直接粘贴图片 (Ctrl+V)"
+ : "等待对方加入P2P网络...
📡 建立连接后即可开始输入文字"
+ }
+ className={`w-full h-40 px-4 py-3 border rounded-lg resize-none text-slate-700 ${
+ connection.isPeerConnected
+ ? "border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-slate-400"
+ : "border-slate-200 bg-slate-50 cursor-not-allowed placeholder-slate-300"
+ }`}
/>
@@ -419,7 +426,10 @@ export const WebRTCTextSender: React.FC = ({ onRestart, o
onClick={() => fileInputRef.current?.click()}
variant="outline"
size="sm"
- className="flex items-center space-x-1"
+ disabled={!connection.isPeerConnected}
+ className={`flex items-center space-x-1 ${
+ !connection.isPeerConnected ? 'cursor-not-allowed opacity-50' : ''
+ }`}
>
添加图片
diff --git a/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts b/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts
new file mode 100644
index 0000000..f59960a
--- /dev/null
+++ b/chuan-next/src/hooks/webrtc/useDesktopShareBusiness.ts
@@ -0,0 +1,407 @@
+import { useState, useRef, useCallback, useEffect } from 'react';
+import { useSharedWebRTCManager } from './useSharedWebRTCManager';
+
+interface DesktopShareState {
+ isSharing: boolean;
+ isViewing: boolean;
+ connectionCode: string;
+ remoteStream: MediaStream | null;
+ error: string | null;
+ isWaitingForPeer: boolean; // 新增:是否等待对方连接
+}
+
+export function useDesktopShareBusiness() {
+ const webRTC = useSharedWebRTCManager();
+ const [state, setState] = useState({
+ isSharing: false,
+ isViewing: false,
+ connectionCode: '',
+ remoteStream: null,
+ error: null,
+ isWaitingForPeer: false,
+ });
+
+ const localStreamRef = useRef(null);
+ const remoteVideoRef = useRef(null);
+ const currentSenderRef = useRef(null);
+
+ const updateState = useCallback((updates: Partial) => {
+ setState(prev => ({ ...prev, ...updates }));
+ }, []);
+
+ // 生成6位房间代码
+ const generateRoomCode = useCallback(() => {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ let result = '';
+ for (let i = 0; i < 6; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return result;
+ }, []);
+
+ // 获取桌面共享流
+ const getDesktopStream = useCallback(async (): Promise => {
+ try {
+ const stream = await navigator.mediaDevices.getDisplayMedia({
+ video: {
+ cursor: 'always',
+ displaySurface: 'monitor',
+ } as DisplayMediaStreamOptions['video'],
+ audio: {
+ echoCancellation: false,
+ noiseSuppression: false,
+ autoGainControl: false,
+ } as DisplayMediaStreamOptions['audio'],
+ });
+
+ console.log('[DesktopShare] 获取桌面流成功:', stream.getTracks().length, '个轨道');
+ return stream;
+ } catch (error) {
+ console.error('[DesktopShare] 获取桌面流失败:', error);
+ throw new Error('无法获取桌面共享权限,请确保允许屏幕共享');
+ }
+ }, []);
+
+ // 设置视频轨道发送
+ const setupVideoSending = useCallback(async (stream: MediaStream) => {
+ console.log('[DesktopShare] 🎬 开始设置视频轨道发送...');
+
+ // 移除之前的轨道(如果存在)
+ if (currentSenderRef.current) {
+ console.log('[DesktopShare] 🗑️ 移除之前的视频轨道');
+ webRTC.removeTrack(currentSenderRef.current);
+ currentSenderRef.current = null;
+ }
+
+ // 添加新的视频轨道到PeerConnection
+ const videoTrack = stream.getVideoTracks()[0];
+ const audioTrack = stream.getAudioTracks()[0];
+
+ if (videoTrack) {
+ console.log('[DesktopShare] 📹 添加视频轨道:', videoTrack.id, videoTrack.readyState);
+ const videoSender = webRTC.addTrack(videoTrack, stream);
+ if (videoSender) {
+ currentSenderRef.current = videoSender;
+ console.log('[DesktopShare] ✅ 视频轨道添加成功');
+ } else {
+ console.warn('[DesktopShare] ⚠️ 视频轨道添加返回null');
+ }
+ } else {
+ console.error('[DesktopShare] ❌ 未找到视频轨道');
+ throw new Error('未找到视频轨道');
+ }
+
+ if (audioTrack) {
+ try {
+ console.log('[DesktopShare] 🎵 添加音频轨道:', audioTrack.id, audioTrack.readyState);
+ const audioSender = webRTC.addTrack(audioTrack, stream);
+ if (audioSender) {
+ console.log('[DesktopShare] ✅ 音频轨道添加成功');
+ } else {
+ console.warn('[DesktopShare] ⚠️ 音频轨道添加返回null');
+ }
+ } catch (error) {
+ console.warn('[DesktopShare] ⚠️ 音频轨道添加失败,继续视频共享:', error);
+ }
+ } else {
+ console.log('[DesktopShare] ℹ️ 未检测到音频轨道(这通常是正常的)');
+ }
+
+ // 轨道添加完成,现在需要重新协商以包含媒体轨道
+ console.log('[DesktopShare] ✅ 桌面共享轨道添加完成,开始重新协商');
+
+ // 检查P2P连接是否已建立
+ if (!webRTC.isPeerConnected) {
+ console.error('[DesktopShare] ❌ P2P连接尚未建立,无法开始媒体传输');
+ throw new Error('P2P连接尚未建立');
+ }
+
+ // 创建新的offer包含媒体轨道
+ console.log('[DesktopShare] 📨 创建包含媒体轨道的新offer进行重新协商');
+ const success = await webRTC.createOfferNow();
+ if (success) {
+ console.log('[DesktopShare] ✅ 媒体轨道重新协商成功');
+ } else {
+ console.error('[DesktopShare] ❌ 媒体轨道重新协商失败');
+ throw new Error('媒体轨道重新协商失败');
+ }
+
+ // 监听流结束事件(用户停止共享)
+ const handleStreamEnded = () => {
+ console.log('[DesktopShare] 🛑 用户停止了屏幕共享');
+ stopSharing();
+ };
+
+ videoTrack?.addEventListener('ended', handleStreamEnded);
+ audioTrack?.addEventListener('ended', handleStreamEnded);
+
+ return () => {
+ videoTrack?.removeEventListener('ended', handleStreamEnded);
+ audioTrack?.removeEventListener('ended', handleStreamEnded);
+ };
+ }, [webRTC]);
+
+ // 处理远程流
+ const handleRemoteStream = useCallback((stream: MediaStream) => {
+ console.log('[DesktopShare] 收到远程流:', stream.getTracks().length, '个轨道');
+ updateState({ remoteStream: stream });
+
+ // 如果有视频元素引用,设置流
+ if (remoteVideoRef.current) {
+ remoteVideoRef.current.srcObject = stream;
+ }
+ }, [updateState]);
+
+ // 创建房间(只建立连接,等待对方加入)
+ const createRoom = useCallback(async (): Promise => {
+ try {
+ updateState({ error: null, isWaitingForPeer: false });
+
+ // 生成房间代码
+ const roomCode = generateRoomCode();
+ console.log('[DesktopShare] 🚀 创建桌面共享房间,代码:', roomCode);
+
+ // 建立WebRTC连接(作为发送方)
+ console.log('[DesktopShare] 📡 正在建立WebRTC连接...');
+ await webRTC.connect(roomCode, 'sender');
+ console.log('[DesktopShare] ✅ WebSocket连接已建立');
+
+ updateState({
+ connectionCode: roomCode,
+ isWaitingForPeer: true, // 标记为等待对方连接
+ });
+
+ console.log('[DesktopShare] 🎯 房间创建完成,等待对方加入建立P2P连接');
+ return roomCode;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '创建房间失败';
+ console.error('[DesktopShare] ❌ 创建房间失败:', error);
+ updateState({ error: errorMessage, connectionCode: '', isWaitingForPeer: false });
+ throw error;
+ }
+ }, [webRTC, generateRoomCode, updateState]);
+
+ // 开始桌面共享(在接收方加入后)
+ const startSharing = useCallback(async (): Promise => {
+ try {
+ // 检查WebSocket连接状态
+ if (!webRTC.isWebSocketConnected) {
+ throw new Error('WebSocket连接未建立,请先创建房间');
+ }
+
+ updateState({ error: null });
+ console.log('[DesktopShare] 📺 正在请求桌面共享权限...');
+
+ // 获取桌面流
+ const stream = await getDesktopStream();
+ localStreamRef.current = stream;
+ console.log('[DesktopShare] ✅ 桌面流获取成功');
+
+ // 设置视频发送(这会添加轨道并创建offer,启动P2P连接)
+ console.log('[DesktopShare] 📤 正在设置视频轨道推送并建立P2P连接...');
+ await setupVideoSending(stream);
+ console.log('[DesktopShare] ✅ 视频轨道推送设置完成');
+
+ updateState({
+ isSharing: true,
+ isWaitingForPeer: false,
+ });
+
+ console.log('[DesktopShare] 🎉 桌面共享已开始');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '开始桌面共享失败';
+ console.error('[DesktopShare] ❌ 开始共享失败:', error);
+ updateState({ error: errorMessage, isSharing: false });
+
+ // 清理资源
+ if (localStreamRef.current) {
+ localStreamRef.current.getTracks().forEach(track => track.stop());
+ localStreamRef.current = null;
+ }
+
+ throw error;
+ }
+ }, [webRTC, getDesktopStream, setupVideoSending, updateState]);
+
+ // 切换桌面共享(重新选择屏幕)
+ const switchDesktop = useCallback(async (): Promise => {
+ try {
+ if (!webRTC.isPeerConnected) {
+ throw new Error('P2P连接未建立');
+ }
+
+ if (!state.isSharing) {
+ throw new Error('当前未在共享桌面');
+ }
+
+ updateState({ error: null });
+ console.log('[DesktopShare] 🔄 正在切换桌面共享...');
+
+ // 获取新的桌面流
+ const newStream = await getDesktopStream();
+
+ // 停止之前的流
+ if (localStreamRef.current) {
+ localStreamRef.current.getTracks().forEach(track => track.stop());
+ }
+
+ localStreamRef.current = newStream;
+ console.log('[DesktopShare] ✅ 新桌面流获取成功');
+
+ // 设置新的视频发送
+ await setupVideoSending(newStream);
+ console.log('[DesktopShare] ✅ 桌面切换完成');
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '切换桌面失败';
+ console.error('[DesktopShare] ❌ 切换桌面失败:', error);
+ updateState({ error: errorMessage });
+ throw error;
+ }
+ }, [webRTC, state.isSharing, getDesktopStream, setupVideoSending, updateState]);
+
+ // 停止桌面共享
+ const stopSharing = useCallback(async (): Promise => {
+ try {
+ console.log('[DesktopShare] 停止桌面共享');
+
+ // 停止本地流
+ if (localStreamRef.current) {
+ localStreamRef.current.getTracks().forEach(track => {
+ track.stop();
+ console.log('[DesktopShare] 停止轨道:', track.kind);
+ });
+ localStreamRef.current = null;
+ }
+
+ // 移除发送器
+ if (currentSenderRef.current) {
+ webRTC.removeTrack(currentSenderRef.current);
+ currentSenderRef.current = null;
+ }
+
+ // 断开WebRTC连接
+ webRTC.disconnect();
+
+ updateState({
+ isSharing: false,
+ connectionCode: '',
+ error: null,
+ isWaitingForPeer: false,
+ });
+
+ console.log('[DesktopShare] 桌面共享已停止');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '停止桌面共享失败';
+ console.error('[DesktopShare] 停止共享失败:', error);
+ updateState({ error: errorMessage });
+ }
+ }, [webRTC, updateState]);
+
+ // 加入桌面共享观看
+ const joinSharing = useCallback(async (code: string): Promise => {
+ try {
+ updateState({ error: null });
+ console.log('[DesktopShare] 🔍 正在加入桌面共享观看:', code);
+
+ // 连接WebRTC
+ console.log('[DesktopShare] 🔗 正在连接WebRTC作为接收方...');
+ await webRTC.connect(code, 'receiver');
+ console.log('[DesktopShare] ✅ WebRTC连接建立完成');
+
+ // 等待连接完全建立
+ console.log('[DesktopShare] ⏳ 等待连接稳定...');
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // 设置远程流处理 - 在连接建立后设置
+ console.log('[DesktopShare] 📡 设置远程流处理器...');
+ webRTC.onTrack((event: RTCTrackEvent) => {
+ console.log('[DesktopShare] 🎥 收到远程轨道:', event.track.kind, event.track.id);
+ console.log('[DesktopShare] 远程流数量:', event.streams.length);
+
+ if (event.streams.length > 0) {
+ const remoteStream = event.streams[0];
+ console.log('[DesktopShare] 🎬 设置远程流,轨道数量:', remoteStream.getTracks().length);
+ handleRemoteStream(remoteStream);
+ } else {
+ console.warn('[DesktopShare] ⚠️ 收到轨道但没有关联的流');
+ }
+ });
+
+ updateState({ isViewing: true });
+ console.log('[DesktopShare] 👁️ 已进入桌面共享观看模式,等待接收流...');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '加入桌面共享失败';
+ console.error('[DesktopShare] ❌ 加入观看失败:', error);
+ updateState({ error: errorMessage, isViewing: false });
+ throw error;
+ }
+ }, [webRTC, handleRemoteStream, updateState]);
+
+ // 停止观看桌面共享
+ const stopViewing = useCallback(async (): Promise => {
+ try {
+ console.log('[DesktopShare] 停止观看桌面共享');
+
+ // 断开WebRTC连接
+ webRTC.disconnect();
+
+ updateState({
+ isViewing: false,
+ remoteStream: null,
+ error: null,
+ });
+
+ console.log('[DesktopShare] 已停止观看桌面共享');
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '停止观看失败';
+ console.error('[DesktopShare] 停止观看失败:', error);
+ updateState({ error: errorMessage });
+ }
+ }, [webRTC, updateState]);
+
+ // 设置远程视频元素引用
+ const setRemoteVideoRef = useCallback((videoElement: HTMLVideoElement | null) => {
+ remoteVideoRef.current = videoElement;
+ if (videoElement && state.remoteStream) {
+ videoElement.srcObject = state.remoteStream;
+ }
+ }, [state.remoteStream]);
+
+ // 清理资源
+ useEffect(() => {
+ return () => {
+ if (localStreamRef.current) {
+ localStreamRef.current.getTracks().forEach(track => track.stop());
+ }
+ };
+ }, []);
+
+ return {
+ // 状态
+ isSharing: state.isSharing,
+ isViewing: state.isViewing,
+ connectionCode: state.connectionCode,
+ remoteStream: state.remoteStream,
+ error: state.error,
+ isWaitingForPeer: state.isWaitingForPeer,
+ isConnected: webRTC.isConnected,
+ isConnecting: webRTC.isConnecting,
+ isWebSocketConnected: webRTC.isWebSocketConnected,
+ isPeerConnected: webRTC.isPeerConnected,
+ // 新增:表示是否可以开始共享(WebSocket已连接且有房间代码)
+ canStartSharing: webRTC.isWebSocketConnected && !!state.connectionCode,
+
+ // 方法
+ createRoom, // 创建房间
+ startSharing, // 选择桌面并建立P2P连接
+ switchDesktop, // 新增:切换桌面
+ stopSharing,
+ joinSharing,
+ stopViewing,
+ setRemoteVideoRef,
+
+ // WebRTC连接状态
+ webRTCError: webRTC.error,
+ };
+}
diff --git a/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts b/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts
index f9efe49..5c0e988 100644
--- a/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts
+++ b/chuan-next/src/hooks/webrtc/useFileTransferBusiness.ts
@@ -3,6 +3,10 @@ import type { WebRTCConnection } from './useSharedWebRTCManager';
// 文件传输状态
interface FileTransferState {
+ isConnecting: boolean;
+ isConnected: boolean;
+ isWebSocketConnected: boolean;
+ connectionError: string | null;
isTransferring: boolean;
progress: number;
error: string | null;
@@ -50,6 +54,10 @@ const CHUNK_SIZE = 256 * 1024; // 256KB
export function useFileTransferBusiness(connection: WebRTCConnection) {
const [state, setState] = useState({
+ isConnecting: false,
+ isConnected: false,
+ isWebSocketConnected: false,
+ connectionError: null,
isTransferring: false,
progress: 0,
error: null,
@@ -177,6 +185,17 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
};
}, [handleMessage, handleData]);
+ // 监听连接状态变化 (直接使用 connection 的状态)
+ useEffect(() => {
+ // 同步连接状态
+ updateState({
+ isConnecting: connection.isConnecting,
+ isConnected: connection.isConnected,
+ isWebSocketConnected: connection.isWebSocketConnected,
+ connectionError: connection.error
+ });
+ }, [connection.isConnecting, connection.isConnected, connection.isWebSocketConnected, connection.error, updateState]);
+
// 连接
const connect = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
return connection.connect(roomCode, role);
@@ -263,6 +282,11 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
// 发送文件列表
const sendFileList = useCallback((fileList: FileInfo[]) => {
+ if (!connection.isPeerConnected) {
+ console.log('P2P连接未建立,等待连接后再发送文件列表');
+ return;
+ }
+
if (connection.getChannelState() !== 'open') {
console.error('数据通道未准备就绪,无法发送文件列表');
return;
@@ -313,13 +337,7 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
}, []);
return {
- // 继承基础连接状态
- isConnected: connection.isConnected,
- isConnecting: connection.isConnecting,
- isWebSocketConnected: connection.isWebSocketConnected,
- connectionError: connection.error,
-
- // 文件传输状态
+ // 文件传输状态(包括连接状态)
...state,
// 操作方法
diff --git a/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts b/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts
index 4d0cb4a..1661d8e 100644
--- a/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts
+++ b/chuan-next/src/hooks/webrtc/useSharedWebRTCManager.ts
@@ -1,11 +1,12 @@
import { useState, useRef, useCallback } from 'react';
-import { config } from '@/lib/config';
+import { getWsUrl } from '@/lib/config';
// 基础连接状态
interface WebRTCState {
isConnected: boolean;
isConnecting: boolean;
isWebSocketConnected: boolean;
+ isPeerConnected: boolean; // 新增:P2P连接状态
error: string | null;
}
@@ -26,6 +27,7 @@ export interface WebRTCConnection {
isConnected: boolean;
isConnecting: boolean;
isWebSocketConnected: boolean;
+ isPeerConnected: boolean; // 新增:P2P连接状态
error: string | null;
// 操作方法
@@ -44,6 +46,13 @@ export interface WebRTCConnection {
// 当前房间信息
currentRoom: { code: string; role: 'sender' | 'receiver' } | null;
+
+ // 媒体轨道方法
+ addTrack: (track: MediaStreamTrack, stream: MediaStream) => RTCRtpSender | null;
+ removeTrack: (sender: RTCRtpSender) => void;
+ onTrack: (callback: (event: RTCTrackEvent) => void) => void;
+ getPeerConnection: () => RTCPeerConnection | null;
+ createOfferNow: () => Promise;
}
/**
@@ -55,6 +64,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
+ isPeerConnected: false,
error: null,
});
@@ -70,12 +80,12 @@ export function useSharedWebRTCManager(): WebRTCConnection {
const messageHandlers = useRef