diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 958ef6d96..2be50eda2 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -2723,6 +2723,14 @@ export function registerIpcHandlers( await waitForNativeMacCaptureStart(proc); const captureStartedAtMs = Date.now(); + const microphoneDefaulted = + request.audio.microphone.enabled && readMicrophoneDefaulted(nativeMacCaptureOutput); + if (microphoneDefaulted) { + console.warn("[native-sck] recording the default input; microphone was not resolved", { + deviceId: request.audio.microphone.deviceId, + deviceName: request.audio.microphone.deviceName, + }); + } nativeMacCursorOffsetMs = cursorCaptureMode === "editable-overlay" ? Math.max(0, captureStartedAtMs - cursorStartTimeMs) @@ -2738,6 +2746,7 @@ export function registerIpcHandlers( recordingId, path: outputPath, helperPath, + microphoneDefaulted, }; } catch (error) { console.error("Failed to start native macOS recording:", error); diff --git a/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift b/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift index 5add8074d..aec518daf 100644 --- a/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift +++ b/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift @@ -465,6 +465,12 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { configuration.setValue(true, forKey: "captureMicrophone") if let deviceId = resolveMicrophoneCaptureDeviceID() { configuration.setValue(deviceId, forKey: "microphoneCaptureDeviceID") + } else { + emit([ + "event": "warning", + "code": "microphone-defaulted", + "message": "The requested microphone could not be resolved; capturing the default input.", + ]) } } else { nativeMicrophoneEnabled = false diff --git a/src/hooks/useScreenRecorder.nativeMacStartWarning.test.tsx b/src/hooks/useScreenRecorder.nativeMacStartWarning.test.tsx new file mode 100644 index 000000000..040eeee6a --- /dev/null +++ b/src/hooks/useScreenRecorder.nativeMacStartWarning.test.tsx @@ -0,0 +1,120 @@ +// @vitest-environment jsdom +import { act, renderHook } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/contexts/I18nContext", () => ({ + useScopedT: () => (key: string) => key, +})); + +vi.mock("sonner", () => ({ + toast: { error: vi.fn(), success: vi.fn(), info: vi.fn(), warning: vi.fn() }, +})); + +import { toast } from "sonner"; +import { useScreenRecorder } from "./useScreenRecorder"; + +type ElectronAPI = Window["electronAPI"]; + +const SOURCE = { id: "screen:0:0", name: "Screen 1", display_id: "1", thumbnail: "" }; + +let api: Record>; + +function stubElectronAPI() { + api = { + getRecordingPrefs: vi.fn(async () => ({ + micEnabled: true, + micDeviceId: "chromium-device-id", + micDeviceName: "USB Microphone", + camEnabled: false, + camDeviceId: null, + systemAudioEnabled: false, + cursorCaptureMode: "system", + })), + getPlatform: vi.fn(() => "darwin"), + getSelectedSource: vi.fn(async () => SOURCE), + isNativeMacCaptureAvailable: vi.fn(async () => ({ success: true, available: true })), + startNativeMacRecording: vi.fn(async () => ({ + success: true, + recordingId: 7, + microphoneDefaulted: true, + })), + stopNativeMacRecording: vi.fn(async () => ({ success: true, discarded: true })), + showCountdownOverlay: vi.fn(async () => true), + setCountdownOverlayValue: vi.fn(async () => true), + hideCountdownOverlay: vi.fn(async () => true), + }; + window.electronAPI = api as unknown as ElectronAPI; +} + +async function settle(ms = 0) { + await act(async () => { + await vi.advanceTimersByTimeAsync(ms); + }); +} + +beforeEach(() => { + vi.useFakeTimers(); + stubElectronAPI(); + vi.mocked(toast.error).mockClear(); +}); + +afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +describe("useScreenRecorder native macOS start warnings", () => { + it("warns but keeps recording when the selected microphone defaults", async () => { + const view = renderHook(() => useScreenRecorder()); + await settle(); + + await act(async () => { + view.result.current.toggleRecording(); + }); + await settle(3_500); + + expect(api.startNativeMacRecording).toHaveBeenCalledWith( + expect.objectContaining({ + audio: { + system: { enabled: false }, + microphone: expect.objectContaining({ + enabled: true, + deviceId: "chromium-device-id", + deviceName: "USB Microphone", + }), + }, + }), + ); + expect(view.result.current.recording).toBe(true); + expect(toast.error).toHaveBeenCalledWith("recording.microphoneDefaulted"); + }); + + it("does not warn after the recording start is cancelled", async () => { + let resolveStart: + | ((result: Awaited>) => void) + | null = null; + api.startNativeMacRecording.mockImplementation( + () => + new Promise((resolve) => { + resolveStart = resolve; + }), + ); + const view = renderHook(() => useScreenRecorder()); + await settle(); + + await act(async () => { + view.result.current.toggleRecording(); + }); + await settle(3_500); + expect(api.startNativeMacRecording).toHaveBeenCalledOnce(); + + view.unmount(); + await act(async () => { + resolveStart?.({ success: true, recordingId: 8, microphoneDefaulted: true }); + await Promise.resolve(); + }); + + expect(api.stopNativeMacRecording).toHaveBeenCalledWith(true); + expect(toast.error).not.toHaveBeenCalled(); + }); +}); diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 14eec5ab4..7507fa153 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -1331,6 +1331,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { await window.electronAPI.stopNativeMacRecording(true); return true; } + if (result.microphoneDefaulted) { + toast.error(t("recording.microphoneDefaulted")); + } // The IPC call above only resolves once the helper's stdout confirms its // screen capture has truly started (see waitForNativeMacCaptureStart in diff --git a/src/lib/nativeMacRecording.ts b/src/lib/nativeMacRecording.ts index e5137c3de..8364310be 100644 --- a/src/lib/nativeMacRecording.ts +++ b/src/lib/nativeMacRecording.ts @@ -88,6 +88,8 @@ export type NativeMacRecordingStartResult = { recordingId?: number; path?: string; helperPath?: string; + /** The helper could not resolve the selected device and is using the system default. */ + microphoneDefaulted?: boolean; error?: string; };