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…");