@@ -314,6 +314,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
314314 const sessionStartedResolverRef = useRef < ( ( value : boolean ) => void ) | null > ( null ) ;
315315 const sessionTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
316316 const pendingSessionPromiseRef = useRef < Promise < boolean > | null > ( null ) ;
317+ const activeSessionModeRef = useRef < 'text' | 'audio' | null > ( null ) ;
317318
318319 // Agent Backend 管理(传入 openPanel 以支持动态刷新)
319320 const { agent, onAgentChange, refreshAgentState } = useLive2DAgentBackend ( {
@@ -392,9 +393,13 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
392393 console . log ( '✅ 收到 session_started,input_mode:' , inputMode ) ;
393394 if ( inputMode === 'text' ) {
394395 setIsTextSessionActive ( true ) ;
396+ activeSessionModeRef . current = 'text' ;
395397 } else if ( inputMode === 'audio' ) {
396398 // audio session 启动意味着 text session 已被替换,重置状态
397399 setIsTextSessionActive ( false ) ;
400+ activeSessionModeRef . current = 'audio' ;
401+ } else {
402+ activeSessionModeRef . current = null ;
398403 }
399404 if ( sessionTimeoutRef . current ) {
400405 clearTimeout ( sessionTimeoutRef . current ) ;
@@ -412,6 +417,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
412417 if ( parsedMsg ?. type === 'session_failed' ) {
413418 console . log ( '❌ 收到 session_failed,input_mode:' , parsedMsg . input_mode ) ;
414419 setIsTextSessionActive ( false ) ;
420+ activeSessionModeRef . current = null ;
415421 if ( sessionTimeoutRef . current ) {
416422 clearTimeout ( sessionTimeoutRef . current ) ;
417423 sessionTimeoutRef . current = null ;
@@ -428,6 +434,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
428434 if ( parsedMsg ?. type === 'session_ended_by_server' ) {
429435 console . log ( '⚠️ 收到 session_ended_by_server,input_mode:' , parsedMsg . input_mode ) ;
430436 setIsTextSessionActive ( false ) ;
437+ activeSessionModeRef . current = null ;
431438 return ;
432439 }
433440
@@ -570,6 +577,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
570577 }
571578 // 连接断开时重置 text session 状态
572579 setIsTextSessionActive ( false ) ;
580+ activeSessionModeRef . current = null ;
573581 }
574582 }
575583 } ) ;
@@ -851,7 +859,39 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
851859 }
852860 } , [ mainManager ] ) ;
853861
854- const handleToggleScreen = useCallback ( async ( next : boolean ) => {
862+ const ensureRealtimeVisionSession = useCallback ( async ( ) : Promise < boolean > => {
863+ const getActiveSessionMode = ( ) : 'text' | 'audio' | null => activeSessionModeRef . current ;
864+
865+ if ( ! audio . isConnected ) {
866+ return false ;
867+ }
868+
869+ if ( getActiveSessionMode ( ) === 'audio' || audio . isRecording ) {
870+ return true ;
871+ }
872+
873+ console . log ( '📤 为实时摄像头准备 audio session' ) ;
874+ audio . sendMessage ( {
875+ action : 'start_session' ,
876+ input_type : 'audio' ,
877+ audio_format : 'PCM_48000HZ_MONO_16BIT' ,
878+ new_session : false ,
879+ } ) ;
880+
881+ const start = Date . now ( ) ;
882+ const timeoutMs = 5000 ;
883+ while ( Date . now ( ) - start < timeoutMs ) {
884+ if ( getActiveSessionMode ( ) === 'audio' ) {
885+ return true ;
886+ }
887+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
888+ }
889+
890+ console . warn ( '⚠️ 实时摄像头切换 audio session 超时' ) ;
891+ return false ;
892+ } , [ audio . isConnected , audio . isRecording , audio . sendMessage ] ) ;
893+
894+ const handleToggleCamera = useCallback ( async ( next : boolean ) => {
855895 if ( ! next ) {
856896 // 停止摄像头
857897 cameraStream . stopStreaming ( ) ;
@@ -864,22 +904,35 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
864904 if ( ! hasPermission ) return ;
865905
866906 // 显示选择对话框
907+ const startCameraStream = async ( selectedFacing : 'front' | 'back' ) => {
908+ statusToastRef . current ?. show ( '正在准备实时视觉...' , 2000 ) ;
909+ const sessionReady = await ensureRealtimeVisionSession ( ) ;
910+ if ( ! sessionReady ) {
911+ statusToastRef . current ?. show ( '实时视觉会话准备失败' , 3000 ) ;
912+ return ;
913+ }
914+
915+ cameraStream . startStreaming ( selectedFacing ) ;
916+ statusToastRef . current ?. show (
917+ selectedFacing === 'front' ? '前置摄像头已开启' : '后置摄像头已开启' ,
918+ 2000
919+ ) ;
920+ } ;
921+
867922 Alert . alert (
868923 '选择摄像头' ,
869924 '请选择要使用的摄像头' ,
870925 [
871926 {
872927 text : '前置摄像头' ,
873928 onPress : ( ) => {
874- cameraStream . startStreaming ( 'front' ) ;
875- statusToastRef . current ?. show ( '前置摄像头已开启' , 2000 ) ;
929+ void startCameraStream ( 'front' ) ;
876930 } ,
877931 } ,
878932 {
879933 text : '后置摄像头' ,
880934 onPress : ( ) => {
881- cameraStream . startStreaming ( 'back' ) ;
882- statusToastRef . current ?. show ( '后置摄像头已开启' , 2000 ) ;
935+ void startCameraStream ( 'back' ) ;
883936 } ,
884937 } ,
885938 {
@@ -888,7 +941,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
888941 } ,
889942 ]
890943 ) ;
891- } , [ cameraStream . startStreaming , cameraStream . stopStreaming , cameraStream . checkAndRequestPermission ] ) ;
944+ } , [ cameraStream . startStreaming , cameraStream . stopStreaming , cameraStream . checkAndRequestPermission , ensureRealtimeVisionSession ] ) ;
892945
893946 const handleGoodbye = useCallback ( ( ) => {
894947 // 如果麦克风正在录音,先停止
@@ -1042,6 +1095,13 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
10421095 return false ;
10431096 }
10441097
1098+ // 连续摄像头需要 realtime/audio session;切回文本前先暂停它,避免图片堆进文本会话队列。
1099+ if ( cameraStream . isStreaming ) {
1100+ console . log ( '📹 切换到文本会话,先暂停实时摄像头' ) ;
1101+ cameraStream . stopStreaming ( ) ;
1102+ statusToastRef . current ?. show ( '发送文本时已暂停实时摄像头' , 2500 ) ;
1103+ }
1104+
10451105 // 如果当前正在录音(语音模式),先停止录音并等待服务端清理旧 session,
10461106 // 避免 start_session(text) 与正在启动/活跃的 audio session 产生竞态
10471107 if ( audio . isRecording ) {
@@ -1095,7 +1155,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
10951155
10961156 pendingSessionPromiseRef . current = promise ;
10971157 return promise ;
1098- } , [ isTextSessionActive , audio . isConnected , audio . isRecording , audio . sendMessage , audio . toggleRecording ] ) ;
1158+ } , [ isTextSessionActive , audio . isConnected , audio . isRecording , audio . sendMessage , audio . toggleRecording , cameraStream . isStreaming , cameraStream . stopStreaming ] ) ;
10991159
11001160 // 图片消息服务
11011161 const imageMessageService = useMemo ( ( ) => new ImageMessageService ( ) , [ ] ) ;
@@ -1273,7 +1333,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
12731333 - 详见:docs/strategy/cross-platform-components.md
12741334
12751335 功能包括:
1276- - 麦克风/屏幕共享切换
1336+ - 麦克风/摄像头切换
12771337 - Agent 设置面板
12781338 - Settings 面板
12791339 - 设置菜单(Live2D设置、API密钥、角色管理等)
@@ -1285,7 +1345,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
12851345 right = { isMobile ? 12 : 24 }
12861346 top = { isMobile ? screenHeight * 0.05 : 24 }
12871347 micEnabled = { toolbarMicEnabled }
1288- screenEnabled = { cameraStream . isStreaming }
1348+ cameraEnabled = { cameraStream . isStreaming }
12891349 goodbyeMode = { toolbarGoodbyeMode }
12901350 openPanel = { toolbarOpenPanel }
12911351 onOpenPanelChange = { setToolbarOpenPanel }
@@ -1294,7 +1354,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
12941354 agent = { agent }
12951355 onAgentChange = { handleToolbarAgentChange }
12961356 onToggleMic = { handleToggleMic }
1297- onToggleScreen = { handleToggleScreen }
1357+ onToggleCamera = { handleToggleCamera }
12981358 onGoodbye = { handleGoodbye }
12991359 onReturn = { handleReturn }
13001360 onSettingsMenuClick = { handleSettingsMenuClick }
0 commit comments