From 3448663afab673e40ce7e8ddce679d5213bccd47 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 4 Nov 2025 08:52:27 +0200 Subject: [PATCH] feat: persist audio preferences to backend config - Add AudioInputAutoEnable and AudioOutputEnabled fields to backend config - Implement RPC methods for get/set audio input auto-enable preference - Load audio output enabled state from config on startup - Make manual microphone toggle call backend to enable audio pipeline - Auto-enable preference now persists across incognito sessions - Reset microphone state to off when reaching login pages - Fix issue where manual mic toggle required auto-enable to be on --- audio.go | 3 +- config.go | 8 +- jsonrpc.go | 203 ++++++++++--------- ui/src/components/popovers/AudioPopover.tsx | 19 +- ui/src/hooks/stores.ts | 5 +- ui/src/routes/devices.$id.settings.audio.tsx | 22 +- ui/src/routes/devices.$id.tsx | 69 +++---- ui/src/routes/login-local.tsx | 3 + ui/src/routes/login.tsx | 6 + 9 files changed, 184 insertions(+), 154 deletions(-) diff --git a/audio.go b/audio.go index 4d616261..af51d16a 100644 --- a/audio.go +++ b/audio.go @@ -29,7 +29,8 @@ var ( func initAudio() { audioLogger = logging.GetDefaultLogger().With().Str("component", "audio-manager").Logger() - audioOutputEnabled.Store(true) + ensureConfigLoaded() + audioOutputEnabled.Store(config.AudioOutputEnabled) audioInputEnabled.Store(true) audioLogger.Debug().Msg("Audio subsystem initialized") diff --git a/config.go b/config.go index d1d0f5b0..219675fb 100644 --- a/config.go +++ b/config.go @@ -107,6 +107,8 @@ type Config struct { DefaultLogLevel string `json:"default_log_level"` VideoSleepAfterSec int `json:"video_sleep_after_sec"` VideoQualityFactor float64 `json:"video_quality_factor"` + AudioInputAutoEnable bool `json:"audio_input_auto_enable"` + AudioOutputEnabled bool `json:"audio_output_enabled"` } func (c *Config) GetDisplayRotation() uint16 { @@ -178,8 +180,10 @@ func getDefaultConfig() Config { _ = confparser.SetDefaultsAndValidate(c) return c }(), - DefaultLogLevel: "INFO", - VideoQualityFactor: 1.0, + DefaultLogLevel: "INFO", + VideoQualityFactor: 1.0, + AudioInputAutoEnable: false, + AudioOutputEnabled: true, } } diff --git a/jsonrpc.go b/jsonrpc.go index f8e07151..6abaa012 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -946,10 +946,16 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { } func rpcGetAudioOutputEnabled() (bool, error) { - return audioOutputEnabled.Load(), nil + ensureConfigLoaded() + return config.AudioOutputEnabled, nil } func rpcSetAudioOutputEnabled(enabled bool) error { + ensureConfigLoaded() + config.AudioOutputEnabled = enabled + if err := SaveConfig(); err != nil { + return err + } return SetAudioOutputEnabled(enabled) } @@ -961,6 +967,17 @@ func rpcSetAudioInputEnabled(enabled bool) error { return SetAudioInputEnabled(enabled) } +func rpcGetAudioInputAutoEnable() (bool, error) { + ensureConfigLoaded() + return config.AudioInputAutoEnable, nil +} + +func rpcSetAudioInputAutoEnable(enabled bool) error { + ensureConfigLoaded() + config.AudioInputAutoEnable = enabled + return SaveConfig() +} + func rpcSetCloudUrl(apiUrl string, appUrl string) error { currentCloudURL := config.CloudURL config.CloudURL = apiUrl @@ -1199,95 +1216,97 @@ func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacro } var rpcHandlers = map[string]RPCHandler{ - "ping": {Func: rpcPing}, - "reboot": {Func: rpcReboot, Params: []string{"force"}}, - "getDeviceID": {Func: rpcGetDeviceID}, - "deregisterDevice": {Func: rpcDeregisterDevice}, - "getCloudState": {Func: rpcGetCloudState}, - "getNetworkState": {Func: rpcGetNetworkState}, - "getNetworkSettings": {Func: rpcGetNetworkSettings}, - "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, - "renewDHCPLease": {Func: rpcRenewDHCPLease}, - "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, - "getKeyDownState": {Func: rpcGetKeysDownState}, - "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, - "keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}}, - "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, - "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, - "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, - "getVideoState": {Func: rpcGetVideoState}, - "getUSBState": {Func: rpcGetUSBState}, - "unmountImage": {Func: rpcUnmountImage}, - "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, - "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, - "getJigglerState": {Func: rpcGetJigglerState}, - "setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}}, - "getJigglerConfig": {Func: rpcGetJigglerConfig}, - "getTimezones": {Func: rpcGetTimezones}, - "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, - "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, - "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, - "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, - "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, - "getEDID": {Func: rpcGetEDID}, - "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, - "getVideoLogStatus": {Func: rpcGetVideoLogStatus}, - "getVideoSleepMode": {Func: rpcGetVideoSleepMode}, - "setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}}, - "getDevChannelState": {Func: rpcGetDevChannelState}, - "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, - "getLocalVersion": {Func: rpcGetLocalVersion}, - "getUpdateStatus": {Func: rpcGetUpdateStatus}, - "tryUpdate": {Func: rpcTryUpdate}, - "getDevModeState": {Func: rpcGetDevModeState}, - "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, - "getSSHKeyState": {Func: rpcGetSSHKeyState}, - "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, - "getTLSState": {Func: rpcGetTLSState}, - "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, - "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, - "getMassStorageMode": {Func: rpcGetMassStorageMode}, - "isUpdatePending": {Func: rpcIsUpdatePending}, - "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, - "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, - "getUsbConfig": {Func: rpcGetUsbConfig}, - "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, - "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, - "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, - "getStorageSpace": {Func: rpcGetStorageSpace}, - "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, - "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, - "listStorageFiles": {Func: rpcListStorageFiles}, - "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, - "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, - "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, - "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, - "resetConfig": {Func: rpcResetConfig}, - "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, - "getDisplayRotation": {Func: rpcGetDisplayRotation}, - "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, - "getBacklightSettings": {Func: rpcGetBacklightSettings}, - "getDCPowerState": {Func: rpcGetDCPowerState}, - "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, - "setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}}, - "getActiveExtension": {Func: rpcGetActiveExtension}, - "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, - "getATXState": {Func: rpcGetATXState}, - "setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}}, - "getSerialSettings": {Func: rpcGetSerialSettings}, - "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, - "getUsbDevices": {Func: rpcGetUsbDevices}, - "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, - "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, - "getAudioOutputEnabled": {Func: rpcGetAudioOutputEnabled}, - "setAudioOutputEnabled": {Func: rpcSetAudioOutputEnabled, Params: []string{"enabled"}}, - "getAudioInputEnabled": {Func: rpcGetAudioInputEnabled}, - "setAudioInputEnabled": {Func: rpcSetAudioInputEnabled, Params: []string{"enabled"}}, - "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, - "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, - "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, - "getKeyboardMacros": {Func: getKeyboardMacros}, - "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, - "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, - "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, + "ping": {Func: rpcPing}, + "reboot": {Func: rpcReboot, Params: []string{"force"}}, + "getDeviceID": {Func: rpcGetDeviceID}, + "deregisterDevice": {Func: rpcDeregisterDevice}, + "getCloudState": {Func: rpcGetCloudState}, + "getNetworkState": {Func: rpcGetNetworkState}, + "getNetworkSettings": {Func: rpcGetNetworkSettings}, + "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, + "renewDHCPLease": {Func: rpcRenewDHCPLease}, + "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, + "getKeyDownState": {Func: rpcGetKeysDownState}, + "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}}, + "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, + "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, + "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, + "getVideoState": {Func: rpcGetVideoState}, + "getUSBState": {Func: rpcGetUSBState}, + "unmountImage": {Func: rpcUnmountImage}, + "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, + "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, + "getJigglerState": {Func: rpcGetJigglerState}, + "setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}}, + "getJigglerConfig": {Func: rpcGetJigglerConfig}, + "getTimezones": {Func: rpcGetTimezones}, + "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, + "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, + "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, + "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, + "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, + "getEDID": {Func: rpcGetEDID}, + "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, + "getVideoLogStatus": {Func: rpcGetVideoLogStatus}, + "getVideoSleepMode": {Func: rpcGetVideoSleepMode}, + "setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}}, + "getDevChannelState": {Func: rpcGetDevChannelState}, + "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, + "getLocalVersion": {Func: rpcGetLocalVersion}, + "getUpdateStatus": {Func: rpcGetUpdateStatus}, + "tryUpdate": {Func: rpcTryUpdate}, + "getDevModeState": {Func: rpcGetDevModeState}, + "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, + "getSSHKeyState": {Func: rpcGetSSHKeyState}, + "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, + "getTLSState": {Func: rpcGetTLSState}, + "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, + "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, + "getMassStorageMode": {Func: rpcGetMassStorageMode}, + "isUpdatePending": {Func: rpcIsUpdatePending}, + "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, + "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, + "getUsbConfig": {Func: rpcGetUsbConfig}, + "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, + "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, + "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, + "getStorageSpace": {Func: rpcGetStorageSpace}, + "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, + "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, + "listStorageFiles": {Func: rpcListStorageFiles}, + "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, + "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, + "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, + "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, + "resetConfig": {Func: rpcResetConfig}, + "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, + "getDisplayRotation": {Func: rpcGetDisplayRotation}, + "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, + "getBacklightSettings": {Func: rpcGetBacklightSettings}, + "getDCPowerState": {Func: rpcGetDCPowerState}, + "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, + "setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}}, + "getActiveExtension": {Func: rpcGetActiveExtension}, + "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, + "getATXState": {Func: rpcGetATXState}, + "setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}}, + "getSerialSettings": {Func: rpcGetSerialSettings}, + "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, + "getUsbDevices": {Func: rpcGetUsbDevices}, + "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, + "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, + "getAudioOutputEnabled": {Func: rpcGetAudioOutputEnabled}, + "setAudioOutputEnabled": {Func: rpcSetAudioOutputEnabled, Params: []string{"enabled"}}, + "getAudioInputEnabled": {Func: rpcGetAudioInputEnabled}, + "setAudioInputEnabled": {Func: rpcSetAudioInputEnabled, Params: []string{"enabled"}}, + "getAudioInputAutoEnable": {Func: rpcGetAudioInputAutoEnable}, + "setAudioInputAutoEnable": {Func: rpcSetAudioInputAutoEnable, Params: []string{"enabled"}}, + "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, + "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, + "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, + "getKeyboardMacros": {Func: getKeyboardMacros}, + "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, + "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, + "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, } diff --git a/ui/src/components/popovers/AudioPopover.tsx b/ui/src/components/popovers/AudioPopover.tsx index 3760843e..fcca38c6 100644 --- a/ui/src/components/popovers/AudioPopover.tsx +++ b/ui/src/components/popovers/AudioPopover.tsx @@ -16,6 +16,7 @@ export default function AudioPopover() { const [audioOutputEnabled, setAudioOutputEnabled] = useState(true); const [usbAudioEnabled, setUsbAudioEnabled] = useState(false); const [loading, setLoading] = useState(false); + const [micLoading, setMicLoading] = useState(false); const isHttps = isSecureContext(); useEffect(() => { @@ -54,6 +55,21 @@ export default function AudioPopover() { }); }, [send]); + const handleMicrophoneToggle = useCallback((enabled: boolean) => { + setMicLoading(true); + send("setAudioInputEnabled", { enabled }, (resp: JsonRpcResponse) => { + setMicLoading(false); + if ("error" in resp) { + const errorMsg = enabled + ? m.audio_input_failed_enable({ error: String(resp.error.data || m.unknown_error()) }) + : m.audio_input_failed_disable({ error: String(resp.error.data || m.unknown_error()) }); + notifications.error(errorMsg); + } else { + setMicrophoneEnabled(enabled); + } + }); + }, [send, setMicrophoneEnabled]); + return (
@@ -80,6 +96,7 @@ export default function AudioPopover() {
setMicrophoneEnabled(e.target.checked)} + onChange={(e) => handleMicrophoneToggle(e.target.checked)} /> diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index c3282f03..00e6e775 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -382,6 +382,8 @@ export interface SettingsState { setMicrophoneEnabled: (enabled: boolean) => void; audioInputAutoEnable: boolean; setAudioInputAutoEnable: (enabled: boolean) => void; + + resetMicrophoneState: () => void; } export const useSettingsStore = create( @@ -430,13 +432,14 @@ export const useSettingsStore = create( videoContrast: 1.0, setVideoContrast: (value: number) => set({ videoContrast: value }), - // Audio settings with defaults audioOutputEnabled: true, setAudioOutputEnabled: (enabled: boolean) => set({ audioOutputEnabled: enabled }), microphoneEnabled: false, setMicrophoneEnabled: (enabled: boolean) => set({ microphoneEnabled: enabled }), audioInputAutoEnable: false, setAudioInputAutoEnable: (enabled: boolean) => set({ audioInputAutoEnable: enabled }), + + resetMicrophoneState: () => set({ microphoneEnabled: false }), }), { name: "settings", diff --git a/ui/src/routes/devices.$id.settings.audio.tsx b/ui/src/routes/devices.$id.settings.audio.tsx index e268ae16..40551440 100644 --- a/ui/src/routes/devices.$id.settings.audio.tsx +++ b/ui/src/routes/devices.$id.settings.audio.tsx @@ -16,20 +16,15 @@ export default function SettingsAudioRoute() { useEffect(() => { send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => { - if ("error" in resp) { - return; - } + if ("error" in resp) return; settings.setAudioOutputEnabled(resp.result as boolean); }); - send("getAudioInputEnabled", {}, (resp: JsonRpcResponse) => { - if ("error" in resp) { - return; - } + send("getAudioInputAutoEnable", {}, (resp: JsonRpcResponse) => { + if ("error" in resp) return; settings.setAudioInputAutoEnable(resp.result as boolean); }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [send]); + }, [send, settings]); const handleAudioOutputEnabledChange = (enabled: boolean) => { send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => { @@ -47,17 +42,12 @@ export default function SettingsAudioRoute() { }; const handleAudioInputAutoEnableChange = (enabled: boolean) => { - send("setAudioInputEnabled", { enabled }, (resp: JsonRpcResponse) => { + send("setAudioInputAutoEnable", { enabled }, (resp: JsonRpcResponse) => { if ("error" in resp) { - const errorMsg = enabled - ? m.audio_input_failed_enable({ error: String(resp.error.data || m.unknown_error()) }) - : m.audio_input_failed_disable({ error: String(resp.error.data || m.unknown_error()) }); - notifications.error(errorMsg); + notifications.error(String(resp.error.data || m.unknown_error())); return; } settings.setAudioInputAutoEnable(enabled); - const successMsg = enabled ? m.audio_input_enabled() : m.audio_input_disabled(); - notifications.success(successMsg); }); }; diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index fc45bf14..500be243 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -538,11 +538,6 @@ export default function KvmIdRoute() { const audioTrans = pc.addTransceiver("audio", { direction: "sendrecv" }); setAudioTransceiver(audioTrans); - // Enable microphone if auto-enable is on (only works over HTTPS or localhost) - if (audioInputAutoEnable && isSecureContext()) { - setMicrophoneEnabled(true); - } - const rpcDataChannel = pc.createDataChannel("rpc"); rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed"); rpcDataChannel.onerror = (ev: Event) => console.error(`Error on DataChannel '${rpcDataChannel.label}': ${ev}`); @@ -606,47 +601,39 @@ export default function KvmIdRoute() { } }, [peerConnectionState, cleanupAndStopReconnecting]); - // Handle dynamic microphone enable/disable useEffect(() => { if (!audioTransceiver || !peerConnection) return; if (microphoneEnabled) { - // Request microphone access - if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - channelCount: 2, - } - }).then((stream) => { - const audioTrack = stream.getAudioTracks()[0]; - if (audioTrack && audioTransceiver.sender) { - audioTransceiver.sender.replaceTrack(audioTrack); - console.log("Microphone enabled"); - } - }).catch((err) => { - console.warn("Microphone access denied or unavailable:", err.message); - setMicrophoneEnabled(false); - }); - } + navigator.mediaDevices?.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + channelCount: 2, + } + }).then((stream) => { + const audioTrack = stream.getAudioTracks()[0]; + if (audioTrack && audioTransceiver.sender) { + audioTransceiver.sender.replaceTrack(audioTrack); + } + }).catch(() => { + setMicrophoneEnabled(false); + }); } else { - // Disable microphone by removing the track if (audioTransceiver.sender.track) { audioTransceiver.sender.track.stop(); audioTransceiver.sender.replaceTrack(null); - console.log("Microphone disabled"); } } - }, [microphoneEnabled, audioTransceiver, peerConnection, setMicrophoneEnabled]); + }, [microphoneEnabled, audioTransceiver, peerConnection]); - // Auto-enable microphone when setting is loaded from backend useEffect(() => { - if (audioInputAutoEnable && audioTransceiver && peerConnection && !microphoneEnabled && isSecureContext()) { + if (!audioTransceiver || !peerConnection || !audioInputAutoEnable || microphoneEnabled) return; + if (isSecureContext()) { setMicrophoneEnabled(true); } - }, [audioInputAutoEnable, audioTransceiver, peerConnection, microphoneEnabled, setMicrophoneEnabled]); + }, [audioInputAutoEnable, audioTransceiver, peerConnection, microphoneEnabled]); // Cleanup effect const { clearInboundRtpStats, clearCandidatePairStats } = useRTCStore(); @@ -806,15 +793,6 @@ export default function KvmIdRoute() { const { send } = useJsonRpc(onJsonRpcRequest); - // Load audio input auto-enable setting from backend on mount - useEffect(() => { - send("getAudioInputEnabled", {}, (resp: JsonRpcResponse) => { - if ("error" in resp) { - return; - } - setAudioInputAutoEnable(resp.result as boolean); - }); - }, [send, setAudioInputAutoEnable]); useEffect(() => { if (rpcDataChannel?.readyState !== "open") return; @@ -827,6 +805,15 @@ export default function KvmIdRoute() { }); }, [rpcDataChannel?.readyState, send, setHdmiState]); + // Load audio input auto-enable preference from backend + useEffect(() => { + if (rpcDataChannel?.readyState !== "open") return; + send("getAudioInputAutoEnable", {}, (resp: JsonRpcResponse) => { + if ("error" in resp) return; + setAudioInputAutoEnable(resp.result as boolean); + }); + }, [rpcDataChannel?.readyState, send, setAudioInputAutoEnable]); + const [needLedState, setNeedLedState] = useState(true); // request keyboard led state from the device diff --git a/ui/src/routes/login-local.tsx b/ui/src/routes/login-local.tsx index 16481568..139faf1c 100644 --- a/ui/src/routes/login-local.tsx +++ b/ui/src/routes/login-local.tsx @@ -16,8 +16,11 @@ import { DeviceStatus } from "@routes/welcome-local"; import { DEVICE_API } from "@/ui.config"; import api from "@/api"; import { m } from "@localizations/messages.js"; +import { useSettingsStore } from "@/hooks/stores"; const loader: LoaderFunction = async () => { + useSettingsStore.getState().resetMicrophoneState(); + const res = await api .GET(`${DEVICE_API}/device/status`) .then(res => res.json() as Promise); diff --git a/ui/src/routes/login.tsx b/ui/src/routes/login.tsx index 15bd73f1..58b0a92f 100644 --- a/ui/src/routes/login.tsx +++ b/ui/src/routes/login.tsx @@ -1,13 +1,19 @@ +import { useEffect } from "react"; import { useLocation, useSearchParams } from "react-router"; import { m } from "@localizations/messages.js"; import AuthLayout from "@components/AuthLayout"; +import { useSettingsStore } from "@/hooks/stores"; export default function LoginRoute() { const [sq] = useSearchParams(); const location = useLocation(); const deviceId = sq.get("deviceId") || location.state?.deviceId; + useEffect(() => { + useSettingsStore.getState().resetMicrophoneState(); + }, []); + if (deviceId) { return (