From 04d4af5ef15a303f88037e844a54d0971c40de94 Mon Sep 17 00:00:00 2001 From: MatrixSeven Date: Mon, 24 Nov 2025 18:05:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=B1=E4=BA=AB=E6=A1=8C=E9=9D=A2?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=BC=80=E5=90=AF=E8=AF=AD=E9=9F=B3=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AE=9E=E7=94=A8=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .chuan.env | 2 +- .gitignore | 5 +- README.md | 4 + chuan-next/build-static.sh | 0 .../src/components/ConnectionStatus.tsx | 27 +- chuan-next/src/components/VoiceIndicator.tsx | 92 +++++ .../webrtc/WebRTCDesktopReceiver.tsx | 219 +++++++++--- .../components/webrtc/WebRTCDesktopSender.tsx | 143 +++++++- chuan-next/src/hooks/connection/types.ts | 6 +- .../src/hooks/connection/useConnectManager.ts | 7 +- .../webrtc/useWebRTCConnectionCore.ts | 111 +++++- .../webrtc/useWebRTCTrackManager.ts | 90 ++--- .../connection/ws/useWebSocketConnection.ts | 4 +- chuan-next/src/hooks/desktop-share/index.ts | 2 + .../hooks/desktop-share/useAudioVisualizer.ts | 122 +++++++ .../desktop-share/useDesktopShareBusiness.ts | 108 +++--- .../desktop-share/useVoiceChatBusiness.ts | 317 ++++++++++++++++++ chuan-next/src/lib/config.ts | 2 +- go.mod | 2 +- go.sum | 5 + 20 files changed, 1088 insertions(+), 180 deletions(-) mode change 100644 => 100755 chuan-next/build-static.sh create mode 100644 chuan-next/src/components/VoiceIndicator.tsx create mode 100644 chuan-next/src/hooks/desktop-share/useAudioVisualizer.ts create mode 100644 chuan-next/src/hooks/desktop-share/useVoiceChatBusiness.ts diff --git a/.chuan.env b/.chuan.env index bf2c2df..825f3f4 100644 --- a/.chuan.env +++ b/.chuan.env @@ -5,7 +5,7 @@ PORT=8080 # FRONTEND_DIR=./dist # TURN服务器配置 -TURN_ENABLED=true +TURN_ENABLED=false TURN_PORT=3478 TURN_USERNAME=chuan TURN_PASSWORD=chuan123 diff --git a/.gitignore b/.gitignore index 1919dce..c3817f5 100644 --- a/.gitignore +++ b/.gitignore @@ -109,4 +109,7 @@ backup/ ./chuan/.next ./internal/web/frontend/* ./file-transfer-server -file-transfer-server \ No newline at end of file +file-transfer-server +./chuan-vue +./chuan-vue/* +chuan-vue \ No newline at end of file diff --git a/README.md b/README.md index 79fb670..12b0362 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ - 🖥️ **多平台支持** - 支持linux/macos/win 单文件部署 ## 🔄 最近更新日志 +### 2025-11-24 +- ✅ **共享桌面** - 共享桌面支持开启语音,提升实用性 ### 2025-09-5 - ✅ **WEBRTC链接恢复** - 关闭页面后在打开,进行数据链接恢复 @@ -179,6 +181,8 @@ make dev cd chuan-next && yarn && yarn dev ``` +[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") + ## 📄 许可证 MIT License diff --git a/chuan-next/build-static.sh b/chuan-next/build-static.sh old mode 100644 new mode 100755 diff --git a/chuan-next/src/components/ConnectionStatus.tsx b/chuan-next/src/components/ConnectionStatus.tsx index c9cc4f5..4966bf7 100644 --- a/chuan-next/src/components/ConnectionStatus.tsx +++ b/chuan-next/src/components/ConnectionStatus.tsx @@ -15,16 +15,18 @@ interface ConnectionStatusProps { } // 连接状态枚举 -const getConnectionStatus = (currentRoom: { code: string; role: Role } | null) => { - - const { getConnectState } = useReadConnectState(); // 确保状态管理器被初始化 - const connection = getConnectState(); - const isWebSocketConnected = connection?.isWebSocketConnected || false; - const isPeerConnected = connection?.isPeerConnected || false; - const isConnecting = connection?.isConnecting || false; - const error = connection?.error || null; - const currentConnectType = connection?.currentConnectType || 'webrtc'; - const isJoinedRoom = connection?.isJoinedRoom || false; +const getConnectionStatus = ( + currentRoom: { code: string; role: Role } | null, + connection: { + isWebSocketConnected: boolean; + isPeerConnected: boolean; + isConnecting: boolean; + error: string | null; + currentConnectType: string; + isJoinedRoom: boolean; + } +) => { + const { isWebSocketConnected, isPeerConnected, isConnecting, error, currentConnectType, isJoinedRoom } = connection; if (!currentRoom) { return { @@ -205,16 +207,15 @@ export function ConnectionStatus(props: ConnectionStatusProps) { isConnecting: webrtcState.isConnecting, error: webrtcState.error, currentConnectType: webrtcState.currentConnectType, + isJoinedRoom: webrtcState.isJoinedRoom, }; - const isConnected = webrtcState.isWebSocketConnected && webrtcState.isPeerConnected; - // 如果是内联模式,只返回状态文字 if (inline) { return {getConnectionStatusText(connection)}; } - const status = getConnectionStatus(currentRoom ?? null); + const status = getConnectionStatus(currentRoom ?? null, connection); if (compact) { return ( diff --git a/chuan-next/src/components/VoiceIndicator.tsx b/chuan-next/src/components/VoiceIndicator.tsx new file mode 100644 index 0000000..21007ed --- /dev/null +++ b/chuan-next/src/components/VoiceIndicator.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { Mic, MicOff } from 'lucide-react'; + +interface VoiceIndicatorProps { + volume: number; // 0-100 + isSpeaking: boolean; + isMuted?: boolean; + className?: string; +} + +export function VoiceIndicator({ + volume, + isSpeaking, + isMuted = false, + className = '', +}: VoiceIndicatorProps) { + // 根据音量计算波纹大小 + const rippleScale = 1 + (volume / 100) * 0.8; // 1.0 到 1.8 + + // 音量条数量(5条) + const barCount = 5; + const activeBars = Math.ceil((volume / 100) * barCount); + + return ( +
+ {/* 麦克风图标和波纹效果 */} +
+ {/* 波纹动画 - 只在说话时显示 */} + {isSpeaking && !isMuted && ( + <> +
+
+ + )} + + {/* 麦克风图标 */} +
+ {isMuted ? ( + + ) : ( + + )} +
+
+ + {/* 音量条 - 10个等级 */} +
+ {Array.from({ length: barCount }).map((_, index) => { + const isActive = index < activeBars && !isMuted; + const height = 8 + index * 1.5; // 递增高度: 8, 9.5, 11, 12.5... 到 21.5 + + return ( +
+ ); + })} +
+
+ ); +} diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx index 6d7eee6..5045ac5 100644 --- a/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx +++ b/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx @@ -1,11 +1,13 @@ "use client"; -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Monitor, Square } from 'lucide-react'; +import { Monitor, Square, Mic, MicOff } from 'lucide-react'; import { useToast } from '@/components/ui/toast-simple'; import { useDesktopShareBusiness } from '@/hooks/desktop-share'; +import { useVoiceChatBusiness } from '@/hooks/desktop-share/useVoiceChatBusiness'; +import { VoiceIndicator } from '@/components/VoiceIndicator'; import DesktopViewer from '@/components/DesktopViewer'; import { ConnectionStatus } from '@/components/ConnectionStatus'; @@ -24,6 +26,26 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec // 使用桌面共享业务逻辑 const desktopShare = useDesktopShareBusiness(); + + // 使用语音通话业务逻辑 + const voiceChat = useVoiceChatBusiness(desktopShare.webRTCConnection); + + // 远程音频元素引用 + const remoteAudioRef = useRef(null); + + // 调试:监控语音状态变化(只监听状态,不监听实时音量) + useEffect(() => { + console.log('[DesktopShareReceiver] 🎤 语音状态变化:', { + isVoiceEnabled: voiceChat.isVoiceEnabled, + isRemoteVoiceActive: voiceChat.isRemoteVoiceActive, + debug: voiceChat._debug + }); + }, [ + voiceChat.isVoiceEnabled, + voiceChat.isRemoteVoiceActive + // 不监听 localVolume, remoteVolume, localIsSpeaking, remoteIsSpeaking + // 这些值每帧都在变化(约60fps),会导致过度渲染 + ]); // 通知父组件连接状态变化 useEffect(() => { @@ -117,7 +139,7 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec } }, [desktopShare, inputCode, isJoiningRoom, showToast]); - // 停止观看 + // 停止观看桌面 const handleStopViewing = useCallback(async () => { try { setIsLoading(true); @@ -132,6 +154,34 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec } }, [desktopShare, showToast]); + // 开启语音 + const handleEnableVoice = useCallback(async () => { + try { + console.log('[DesktopShareReceiver] 用户点击开启语音'); + await voiceChat.enableVoice(); + showToast('语音已开启', 'success'); + } catch (error) { + console.error('[DesktopShareReceiver] 开启语音失败:', error); + let errorMessage = '开启语音失败'; + + if (error instanceof Error) { + if (error.message.includes('麦克风权限') || error.message.includes('Permission')) { + errorMessage = '无法访问麦克风,请检查浏览器权限设置'; + } else if (error.message.includes('P2P连接')) { + errorMessage = '请先等待连接建立'; + } else if (error.message.includes('NotFoundError') || error.message.includes('设备')) { + errorMessage = '未检测到麦克风设备'; + } else if (error.message.includes('NotAllowedError')) { + errorMessage = '麦克风权限被拒绝,请在浏览器设置中允许使用麦克风'; + } else { + errorMessage = error.message; + } + } + + showToast(errorMessage, 'error'); + } + }, [voiceChat, showToast]); + // 如果有初始代码且还未加入观看,自动尝试加入 React.useEffect(() => { console.log('[WebRTCDesktopReceiver] useEffect 触发, 参数:', { @@ -320,50 +370,143 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec />
- {/* 观看中的控制面板 */} -
-
-
- - 观看中 + {/* 观看中的控制面板 - 移动端优化 */} +
+
+
+ {/* 状态指示 */} +
+ + 观看中 +
+ + {/* 对方说话提示 - 移动端全宽 */} + {voiceChat.isRemoteVoiceActive && voiceChat.remoteIsSpeaking && ( +
+
+ + 对方正在讲话 +
+ )} + + {/* 按钮组 - 移动端全宽横向 */} +
+ + + +
-
{/* 桌面显示区域 */} - {desktopShare.remoteStream ? ( - - ) : ( -
-
- -

等待接收桌面画面...

-

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

- -
-
- 等待桌面流... -
+
+ {desktopShare.remoteStream ? ( + + ) : ( +
+
+ +

等待接收桌面画面...

+

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

+ +
+
+ 等待桌面流... +
+
-
- )} + )} + + {/* 语音状态指示器 - 始终显示,点击切换 */} + {desktopShare.remoteStream && ( +
+
voiceChat.disableVoice() : handleEnableVoice} + title={voiceChat.isVoiceEnabled ? "点击关闭发言" : "点击开启发言"} + > +
+
+
+ {voiceChat.isVoiceEnabled ? ( + + ) : ( + + )} +
+
+ 我的发言 + + {voiceChat.isVoiceEnabled ? '点击关闭' : '点击开启'} + +
+
+ {voiceChat.isVoiceEnabled && ( + + )} +
+
+
+ )} +
)}
+ + {/* 隐藏的音频元素用于播放远程音频 */} +
); diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx index 9a16532..a44cfd6 100644 --- a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx +++ b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx @@ -5,7 +5,9 @@ import RoomInfoDisplay from '@/components/RoomInfoDisplay'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/toast-simple'; import { useDesktopShareBusiness } from '@/hooks/desktop-share'; -import { Monitor, Repeat, Share, Square } from 'lucide-react'; +import { useVoiceChatBusiness } from '@/hooks/desktop-share/useVoiceChatBusiness'; +import { VoiceIndicator } from '@/components/VoiceIndicator'; +import { Monitor, Repeat, Share, Square, Mic, MicOff } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; interface WebRTCDesktopSenderProps { @@ -19,6 +21,23 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W // 使用桌面共享业务逻辑 const desktopShare = useDesktopShareBusiness(); + + // 使用语音通话业务逻辑 - 传入同一个connection实例 + const voiceChat = useVoiceChatBusiness(desktopShare.webRTCConnection); + + // 调试:监控语音状态变化(只监听状态,不监听实时音量) + useEffect(() => { + console.log('[DesktopShareSender] 🎤 语音状态变化:', { + isVoiceEnabled: voiceChat.isVoiceEnabled, + isRemoteVoiceActive: voiceChat.isRemoteVoiceActive, + debug: voiceChat._debug + }); + }, [ + voiceChat.isVoiceEnabled, + voiceChat.isRemoteVoiceActive + // 不监听 localVolume, remoteVolume, localIsSpeaking, remoteIsSpeaking + // 这些值每帧都在变化(约60fps),会导致过度渲染 + ]); // 调试:监控localStream状态变化 useEffect(() => { @@ -34,6 +53,11 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W // 保持本地视频元素的引用 const localVideoRef = useRef(null); + // 设置远程音频元素的回调 + const setRemoteAudioRef = useCallback((audioElement: HTMLAudioElement | null) => { + voiceChat.setRemoteAudioRef(audioElement); + }, [voiceChat]); + // 处理本地流变化,确保视频正确显示 useEffect(() => { if (localVideoRef.current && desktopShare.localStream) { @@ -213,6 +237,34 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W } }, [desktopShare, showToast]); + // 开启语音 + const handleEnableVoice = useCallback(async () => { + try { + console.log('[DesktopShareSender] 用户点击开启语音'); + await voiceChat.enableVoice(); + showToast('语音已开启', 'success'); + } catch (error) { + console.error('[DesktopShareSender] 开启语音失败:', error); + let errorMessage = '开启语音失败'; + + if (error instanceof Error) { + if (error.message.includes('麦克风权限') || error.message.includes('Permission')) { + errorMessage = '无法访问麦克风,请检查浏览器权限设置'; + } else if (error.message.includes('P2P连接')) { + errorMessage = '请先等待对方加入'; + } else if (error.message.includes('NotFoundError') || error.message.includes('设备')) { + errorMessage = '未检测到麦克风设备'; + } else if (error.message.includes('NotAllowedError')) { + errorMessage = '麦克风权限被拒绝,请在浏览器设置中允许使用麦克风'; + } else { + errorMessage = error.message; + } + } + + showToast(errorMessage, 'error'); + } + }, [voiceChat, showToast]); + return (
@@ -293,16 +345,16 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W {/* 控制按钮 */} {desktopShare.isSharing && (
- + + + {/* 语音控制按钮 */} +
)}
@@ -347,9 +423,44 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W
)} + + {/* 语音状态指示器 - 始终显示,点击切换 */} +
+
+
+
+ {voiceChat.isVoiceEnabled ? ( + + ) : ( + + )} +
+
+ 我的发言 + + {voiceChat.isVoiceEnabled ? '点击关闭' : '点击开启'} + +
+ {voiceChat.isVoiceEnabled && ( + + )} +
+
+
)} - )} @@ -376,6 +487,14 @@ export default function WebRTCDesktopSender({ className, onConnectionChange }: W showToast('观看链接已复制', 'success'); }} /> + + {/* 隐藏的远程音频播放元素 - 用于播放观看方的语音 */} +