mirror of https://github.com/jetkvm/kvm.git
Stability: prevent race condition when clicking on Mic Start, Stop buttons in quick succession
This commit is contained in:
parent
3c1f96d49c
commit
94ca3fa3f4
|
@ -64,8 +64,7 @@ func (aim *AudioInputManager) Stop() {
|
|||
aim.logger.Info().Msg("Stopping audio input manager")
|
||||
|
||||
// Stop the non-blocking audio input stream
|
||||
// Note: This is handled by the global non-blocking audio manager
|
||||
// Individual input streams are managed centrally
|
||||
StopNonBlockingAudioInput()
|
||||
|
||||
// Drain the input buffer
|
||||
go func() {
|
||||
|
@ -78,6 +77,8 @@ func (aim *AudioInputManager) Stop() {
|
|||
}
|
||||
}
|
||||
}()
|
||||
|
||||
aim.logger.Info().Msg("Audio input manager stopped")
|
||||
}
|
||||
|
||||
// WriteOpusFrame writes an Opus frame to the input buffer
|
||||
|
|
|
@ -413,6 +413,10 @@ func (nam *NonBlockingAudioManager) StopAudioInput() {
|
|||
// Stop only the input coordinator
|
||||
atomic.StoreInt32(&nam.inputRunning, 0)
|
||||
|
||||
// Allow coordinator thread to process the stop signal and update state
|
||||
// This prevents race conditions in state queries immediately after stopping
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
nam.logger.Info().Msg("audio input stopped")
|
||||
}
|
||||
|
||||
|
|
|
@ -327,11 +327,18 @@ export function useMicrophone() {
|
|||
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
// If this is a retry, first try to stop the backend microphone to reset state
|
||||
// If this is a retry, first try to reset the backend microphone state
|
||||
if (attempt > 1) {
|
||||
console.log(`Backend start attempt ${attempt}, first trying to reset backend state...`);
|
||||
try {
|
||||
await api.POST("/microphone/stop", {});
|
||||
// Try the new reset endpoint first
|
||||
const resetResp = await api.POST("/microphone/reset", {});
|
||||
if (resetResp.ok) {
|
||||
console.log("Backend reset successful");
|
||||
} else {
|
||||
// Fallback to stop
|
||||
await api.POST("/microphone/stop", {});
|
||||
}
|
||||
// Wait a bit for the backend to reset
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
} catch (resetError) {
|
||||
|
@ -358,6 +365,24 @@ export function useMicrophone() {
|
|||
console.log("Backend response data:", responseData);
|
||||
if (responseData.status === "already running") {
|
||||
console.info("Backend microphone was already running");
|
||||
|
||||
// If we're on the first attempt and backend says "already running",
|
||||
// but frontend thinks it's not active, this might be a stuck state
|
||||
if (attempt === 1 && !isMicrophoneActive) {
|
||||
console.warn("Backend reports 'already running' but frontend is not active - possible stuck state");
|
||||
console.log("Attempting to reset backend state and retry...");
|
||||
|
||||
try {
|
||||
const resetResp = await api.POST("/microphone/reset", {});
|
||||
if (resetResp.ok) {
|
||||
console.log("Backend reset successful, retrying start...");
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
continue; // Retry the start
|
||||
}
|
||||
} catch (resetError) {
|
||||
console.warn("Failed to reset stuck backend state:", resetError);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Backend microphone start successful");
|
||||
backendSuccess = true;
|
||||
|
@ -457,15 +482,47 @@ export function useMicrophone() {
|
|||
const resetBackendMicrophoneState = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
console.log("Resetting backend microphone state...");
|
||||
await api.POST("/microphone/stop", {});
|
||||
// Wait for backend to process the stop
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return true;
|
||||
const response = await api.POST("/microphone/reset", {});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log("Backend microphone reset successful:", data);
|
||||
|
||||
// Update frontend state to match backend
|
||||
setMicrophoneActive(false);
|
||||
setMicrophoneMuted(false);
|
||||
|
||||
// Clean up any orphaned streams
|
||||
if (microphoneStreamRef.current) {
|
||||
console.log("Cleaning up orphaned stream after reset");
|
||||
await stopMicrophoneStream();
|
||||
}
|
||||
|
||||
// Wait a bit for everything to settle
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// Sync state to ensure consistency
|
||||
await syncMicrophoneState();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.error("Backend microphone reset failed:", response.status);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to reset backend microphone state:", error);
|
||||
return false;
|
||||
// Fallback to old method
|
||||
try {
|
||||
console.log("Trying fallback reset method...");
|
||||
await api.POST("/microphone/stop", {});
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return true;
|
||||
} catch (fallbackError) {
|
||||
console.error("Fallback reset also failed:", fallbackError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [setMicrophoneActive, setMicrophoneMuted, stopMicrophoneStream, syncMicrophoneState]);
|
||||
|
||||
// Stop microphone
|
||||
const stopMicrophone = useCallback(async (): Promise<{ success: boolean; error?: MicrophoneError }> => {
|
||||
|
|
31
web.go
31
web.go
|
@ -398,6 +398,37 @@ func setupRouter() *gin.Engine {
|
|||
})
|
||||
})
|
||||
|
||||
protected.POST("/microphone/reset", func(c *gin.Context) {
|
||||
if currentSession == nil {
|
||||
c.JSON(400, gin.H{"error": "no active session"})
|
||||
return
|
||||
}
|
||||
|
||||
if currentSession.AudioInputManager == nil {
|
||||
c.JSON(500, gin.H{"error": "audio input manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info().Msg("forcing microphone state reset")
|
||||
|
||||
// Force stop both the AudioInputManager and NonBlockingAudioManager
|
||||
currentSession.AudioInputManager.Stop()
|
||||
audio.StopNonBlockingAudioInput()
|
||||
|
||||
// Wait a bit to ensure everything is stopped
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Broadcast microphone state change via WebSocket
|
||||
broadcaster := audio.GetAudioEventBroadcaster()
|
||||
broadcaster.BroadcastMicrophoneStateChanged(false, true)
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": "reset",
|
||||
"audio_input_running": currentSession.AudioInputManager.IsRunning(),
|
||||
"nonblocking_input_running": audio.IsNonBlockingAudioInputRunning(),
|
||||
})
|
||||
})
|
||||
|
||||
// Catch-all route for SPA
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
if c.Request.Method == "GET" && c.NegotiateFormat(gin.MIMEHTML) == gin.MIMEHTML {
|
||||
|
|
Loading…
Reference in New Issue