diff --git a/src/cli/CliRecordRunner.test.tsx b/src/cli/CliRecordRunner.test.tsx index 260a2d95aa..f0448704f9 100644 --- a/src/cli/CliRecordRunner.test.tsx +++ b/src/cli/CliRecordRunner.test.tsx @@ -82,6 +82,36 @@ describe("CliRecordRunner", () => { expect(recorder.startRecordingImmediately).not.toHaveBeenCalled(); }); + it("fails, and stops the starting capture, when the camera turns on but cannot be opened", async () => { + vi.mocked(window.electronAPI.cliGetRequest).mockResolvedValueOnce({ ...request, camera: true }); + recorder.setWebcamEnabled.mockImplementationOnce(async () => { + recorder.webcamEnabled = true; + return true; + }); + try { + const { rerender } = render(); + await waitFor(() => expect(recorder.startRecordingImmediately).toHaveBeenCalledTimes(1)); + // The acquire failed (another app holds the camera): the recorder turns it back off. + recorder.webcamEnabled = false; + rerender(); + await waitFor(() => + expect(window.electronAPI.cliDone).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + error: expect.stringContaining("camera could not be opened"), + }), + ), + ); + recorder.recording = true; + rerender(); + await waitFor(() => expect(recorder.toggleRecording).toHaveBeenCalledTimes(1)); + expect(window.electronAPI.cliLog).not.toHaveBeenCalledWith("info", "Recording started"); + } finally { + recorder.webcamEnabled = false; + recorder.recording = false; + } + }); + it("leaves the camera off unless asked", async () => { recorder.setWebcamEnabled.mockClear(); render(); diff --git a/src/cli/CliRecordRunner.tsx b/src/cli/CliRecordRunner.tsx index 933c1892e3..8ea924722c 100644 --- a/src/cli/CliRecordRunner.tsx +++ b/src/cli/CliRecordRunner.tsx @@ -253,9 +253,28 @@ export function CliRecordRunner() { recorder.cursorCaptureMode, ]); + // A camera that cannot be opened (held by another app, OpenScreen's own HUD among them on + // Linux, or unplugged) turns itself back off in the recorder, which then records without it. + // A take asked to have the camera fails instead of reporting success without it (#1303). + // biome-ignore lint/correctness/useExhaustiveDependencies: fail is stable + useEffect(() => { + if (!requestReady?.camera || recorder.webcamEnabled) return; + if (phaseRef.current === "done" || recordingStartedAtRef.current !== null) return; + void fail( + new Error( + "The camera could not be opened: another app is using it (OpenScreen itself, when it is running), or it was disconnected", + ), + ); + }, [requestReady, recorder.webcamEnabled]); + // Recording state transitions: report start and arm the duration timer. useEffect(() => { const request = requestRef.current; + // The take failed while its capture was starting: stop it, nobody is waiting for it. + if (recording && phaseRef.current === "done" && recordingStartedAtRef.current === null) { + toggleRecordingRef.current(); + return; + } if (recording && recordingStartedAtRef.current === null) { recordingStartedAtRef.current = Date.now(); setStatus("Recording…");