From aafa44014c8d3e2aa73fee1dfe1662de9f016485 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Sun, 20 Sep 2026 08:11:06 +0530 Subject: [PATCH] Fix: stop mic fallback recorder when cancelling native recording Cancelling a native-capture recording (clicking the "X"/cancel button) never stopped the mic fallback recorder's getUserMedia stream. The stale mic track stayed open, so starting the next recording opened a second concurrent mic stream and produced silent/dead audio until the user manually reselected the microphone. cancelRecording's native-capture branch now stops the mic fallback recorder alongside discarding the native capture, mirroring the cleanup stopRecording already performs. Fixes #699 Co-Authored-By: Claude Sonnet 5 --- src/hooks/useScreenRecorder.test.ts | 19 +++++++++++++++++++ src/hooks/useScreenRecorder.ts | 10 ++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/hooks/useScreenRecorder.test.ts b/src/hooks/useScreenRecorder.test.ts index 1ddceb4e1..7b48fcd12 100644 --- a/src/hooks/useScreenRecorder.test.ts +++ b/src/hooks/useScreenRecorder.test.ts @@ -397,6 +397,7 @@ function cancelRecording( chunks: { current: Blob[] }, webcamRecorder?: ReturnType | null, webcamChunks?: { current: Blob[] }, + stopMicFallbackRecorder?: () => Promise, ) { if (webcamChunks) webcamChunks.current = []; if (webcamRecorder && webcamRecorder.state !== "inactive") { @@ -404,6 +405,7 @@ function cancelRecording( } if (isNativeRecording) { + void stopMicFallbackRecorder?.(); return { cancelled: true, wasNative: true }; } @@ -740,6 +742,23 @@ describe("useScreenRecorder state machine", () => { expect(recorder.stop).not.toHaveBeenCalled(); }); + it("stops the mic fallback recorder when cancelling native recording", () => { + const chunks = { current: [] as Blob[] }; + const stopMicFallbackRecorder = vi.fn(() => Promise.resolve(null)); + + const result = cancelRecording( + recorder, + true, + chunks, + null, + undefined, + stopMicFallbackRecorder, + ); + + expect(result.wasNative).toBe(true); + expect(stopMicFallbackRecorder).toHaveBeenCalled(); + }); + it("handles cancel when recorder is already inactive", () => { const inactiveRecorder = createMockMediaRecorder("inactive"); const chunks = { current: [new Blob(["data"])] }; diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 459652415..c05942f03 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -2394,7 +2394,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setRecording(false); window.electronAPI?.setRecordingState(false); void (async () => { - await discardActiveNativeCapture(); + await Promise.allSettled([discardActiveNativeCapture(), stopMicFallbackRecorder()]); })(); return; } @@ -2408,7 +2408,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setRecording(false); window.electronAPI?.setRecordingState(false); } - }, [cleanupCapturedMedia, discardActiveNativeCapture, markRecordingResumed, recording]); + }, [ + cleanupCapturedMedia, + discardActiveNativeCapture, + markRecordingResumed, + recording, + stopMicFallbackRecorder, + ]); const toggleRecording = async () => { if (starting || countdownActive || finalizing) {