From f2ad918dfd0f6575f4ca9eb9d0e7e543f41a12b0 Mon Sep 17 00:00:00 2001 From: Alex P Date: Sun, 21 Sep 2025 17:52:55 +0300 Subject: [PATCH] [WIP] Improvements: Improve audio resume mechanism after Hardware Settings deactivation & reactivation --- .../popovers/AudioControlPopover.tsx | 35 +-------- ui/src/hooks/useMicrophone.ts | 75 ++++++++++++++++++- 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/ui/src/components/popovers/AudioControlPopover.tsx b/ui/src/components/popovers/AudioControlPopover.tsx index 2988eaa0..594a8838 100644 --- a/ui/src/components/popovers/AudioControlPopover.tsx +++ b/ui/src/components/popovers/AudioControlPopover.tsx @@ -217,44 +217,17 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP setIsLoading(true); try { - // Use RPC for device communication - works for both local and cloud - if (rpcDataChannel?.readyState !== "open") { - throw new Error("Device connection not available"); - } - if (isMicrophoneActiveFromHook) { - // Disable: Stop microphone subprocess via RPC AND remove WebRTC tracks locally - await new Promise((resolve, reject) => { - send("microphoneStop", {}, (resp: JsonRpcResponse) => { - if ("error" in resp) { - reject(new Error(resp.error.message)); - } else { - resolve(); - } - }); - }); - - // Also stop local WebRTC stream + // Disable: Use the hook's stopMicrophone which handles both RPC and local cleanup const result = await stopMicrophone(); if (!result.success) { - console.warn("Local microphone stop failed:", result.error?.message); + throw new Error(result.error?.message || "Failed to stop microphone"); } } else { - // Enable: Start microphone subprocess via RPC AND add WebRTC tracks locally - await new Promise((resolve, reject) => { - send("microphoneStart", {}, (resp: JsonRpcResponse) => { - if ("error" in resp) { - reject(new Error(resp.error.message)); - } else { - resolve(); - } - }); - }); - - // Also start local WebRTC stream + // Enable: Use the hook's startMicrophone which handles both RPC and local setup const result = await startMicrophone(); if (!result.success) { - throw new Error(result.error?.message || "Failed to start local microphone"); + throw new Error(result.error?.message || "Failed to start microphone"); } } } catch (error) { diff --git a/ui/src/hooks/useMicrophone.ts b/ui/src/hooks/useMicrophone.ts index 25124743..40744222 100644 --- a/ui/src/hooks/useMicrophone.ts +++ b/ui/src/hooks/useMicrophone.ts @@ -2,6 +2,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useRTCStore, useSettingsStore } from "@/hooks/stores"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; +import { useUsbDeviceConfig } from "@/hooks/useUsbDeviceConfig"; +import { useAudioEvents, AudioDeviceChangedData } from "@/hooks/useAudioEvents"; import { devLog, devInfo, devWarn, devError, devOnly } from "@/utils/debug"; import { AUDIO_CONFIG } from "@/config/constants"; @@ -27,6 +29,16 @@ export function useMicrophone() { const { microphoneWasEnabled, setMicrophoneWasEnabled } = useSettingsStore(); const { send } = useJsonRpc(); + // Check USB audio status and handle microphone restoration when USB audio is re-enabled + const { usbDeviceConfig } = useUsbDeviceConfig(); + const isUsbAudioEnabled = usbDeviceConfig?.audio ?? true; + + // Track microphone state when USB audio gets disabled, so we can restore it when re-enabled + const [microphoneWasActiveBeforeUsbDisable, setMicrophoneWasActiveBeforeUsbDisable] = useState(false); + + // Track previous USB audio state to detect changes + const prevUsbAudioEnabled = useRef(null); + // RPC helper functions to replace HTTP API calls const rpcMicrophoneStart = useCallback((): Promise => { return new Promise((resolve, reject) => { @@ -261,9 +273,19 @@ export function useMicrophone() { }); } - // Notify backend that microphone is started + // Notify backend that microphone is started - only if USB audio is enabled + if (!isUsbAudioEnabled) { + devInfo("USB audio is disabled, skipping backend microphone start"); + // Still set frontend state as active since the stream was successfully created + setMicrophoneActive(true); + setMicrophoneMuted(false); + setMicrophoneWasEnabled(true); + isStartingRef.current = false; + setIsStarting(false); + return { success: true }; + } - // Retry logic for backend failures + // Retry logic for backend failures let backendSuccess = false; let lastError: Error | string | null = null; @@ -372,7 +394,7 @@ export function useMicrophone() { setIsStarting(false); return { success: false, error: micError }; } - }, [peerConnection, setMicrophoneStream, setMicrophoneSender, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, stopMicrophoneStream, isStarting, isStopping, isToggling, rpcMicrophoneStart, rpcDataChannel?.readyState, send]); + }, [peerConnection, setMicrophoneStream, setMicrophoneSender, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, stopMicrophoneStream, isStarting, isStopping, isToggling, rpcMicrophoneStart, rpcDataChannel?.readyState, send, isUsbAudioEnabled]); @@ -575,6 +597,53 @@ export function useMicrophone() { return () => clearTimeout(timer); }, [syncMicrophoneState, microphoneWasEnabled, isMicrophoneActive, peerConnection, startMicrophone, rpcDataChannel?.readyState]); + // Handle USB audio enable/disable changes + useEffect(() => { + // If this is the first run, just store the current state + if (prevUsbAudioEnabled.current === null) { + prevUsbAudioEnabled.current = isUsbAudioEnabled; + return; + } + + // USB audio was just disabled + if (prevUsbAudioEnabled.current && !isUsbAudioEnabled) { + devInfo("USB audio disabled - storing microphone state"); + setMicrophoneWasActiveBeforeUsbDisable(isMicrophoneActive); + // Clear the enabled flag to prevent auto-restore attempts + if (isMicrophoneActive) { + setMicrophoneWasEnabled(false); + } + } + + // USB audio was just re-enabled + else if (!prevUsbAudioEnabled.current && isUsbAudioEnabled) { + devInfo("USB audio re-enabled - checking if microphone should be restored"); + + // If microphone was active before USB was disabled, restore it + if (microphoneWasActiveBeforeUsbDisable && !isMicrophoneActive && rpcDataChannel?.readyState === "open") { + devInfo("Restoring microphone after USB audio re-enabled"); + setTimeout(async () => { + try { + const result = await startMicrophone(); + if (result.success) { + devInfo("Microphone successfully restored after USB audio re-enable"); + setMicrophoneWasEnabled(true); + } else { + devWarn("Failed to restore microphone after USB audio re-enable:", result.error); + } + } catch (error) { + devWarn("Error restoring microphone after USB audio re-enable:", error); + } + }, 500); // Small delay to ensure USB device reconfiguration is complete + } + + // Clear the stored state + setMicrophoneWasActiveBeforeUsbDisable(false); + } + + prevUsbAudioEnabled.current = isUsbAudioEnabled; + }, [isUsbAudioEnabled, isMicrophoneActive, microphoneWasActiveBeforeUsbDisable, startMicrophone, setMicrophoneWasEnabled, rpcDataChannel?.readyState]); + // Cleanup on unmount - use ref to avoid dependency on stopMicrophoneStream useEffect(() => { return () => {