Skip to content

Commit 1f99fcb

Browse files
Merge pull request #325 from dheerajmr01/fix/camera-bugs
fix: camera light flashes and turns off when clicking webcam button (…
2 parents 2b1c931 + 210baee commit 1f99fcb

7 files changed

Lines changed: 105 additions & 29 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dist-ssr
2727
*.sw?
2828
release/**
2929
*.kiro/
30+
.claude/
3031
# npx electron-builder --mac --win
3132

3233
# Playwright

src/components/launch/LaunchWindow.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ export function LaunchWindow() {
526526
onClick={async () => {
527527
await setWebcamEnabled(!webcamEnabled);
528528
}}
529+
disabled={recording}
529530
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
530531
>
531532
{webcamEnabled

src/hooks/useScreenRecorder.ts

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
110110
const allowAutoFinalize = useRef(false);
111111
const discardRecordingId = useRef<number | null>(null);
112112
const restarting = useRef(false);
113+
const webcamReady = useRef(false);
114+
const webcamAcquireId = useRef(0);
113115

114116
const getRecordingDurationMs = useCallback(() => {
115117
const segmentDuration =
@@ -158,10 +160,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
158160
microphoneStream.current.getTracks().forEach((track) => track.stop());
159161
microphoneStream.current = null;
160162
}
161-
if (webcamStream.current) {
162-
webcamStream.current.getTracks().forEach((track) => track.stop());
163-
webcamStream.current = null;
164-
}
165163
if (mixingContext.current) {
166164
mixingContext.current.close().catch(() => {
167165
// Ignore close errors during recorder teardown.
@@ -194,6 +192,85 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
194192
[t],
195193
);
196194

195+
useEffect(() => {
196+
if (!webcamEnabled) return;
197+
198+
let cancelled = false;
199+
let acquiredStream: MediaStream | null = null;
200+
const thisAcquireId = ++webcamAcquireId.current;
201+
webcamReady.current = false;
202+
203+
const acquire = async () => {
204+
try {
205+
const stream = await navigator.mediaDevices.getUserMedia({
206+
audio: false,
207+
video: webcamDeviceId
208+
? {
209+
deviceId: { exact: webcamDeviceId },
210+
width: { ideal: WEBCAM_TARGET_WIDTH },
211+
height: { ideal: WEBCAM_TARGET_HEIGHT },
212+
frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE },
213+
}
214+
: {
215+
width: { ideal: WEBCAM_TARGET_WIDTH },
216+
height: { ideal: WEBCAM_TARGET_HEIGHT },
217+
frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE },
218+
},
219+
});
220+
221+
if (cancelled || thisAcquireId !== webcamAcquireId.current) {
222+
stream.getTracks().forEach((track) => {
223+
track.onended = null;
224+
track.stop();
225+
});
226+
return;
227+
}
228+
229+
acquiredStream = stream;
230+
stream.getVideoTracks().forEach((track) => {
231+
track.onended = () => {
232+
webcamStream.current = null;
233+
if (!restarting.current) {
234+
setWebcamEnabledState(false);
235+
toast.error(t("recording.cameraDisconnected"));
236+
}
237+
};
238+
});
239+
webcamStream.current = stream;
240+
webcamReady.current = true;
241+
} catch (cameraError) {
242+
if (!cancelled) {
243+
console.warn("Failed to get webcam access:", cameraError);
244+
setWebcamEnabledState(false);
245+
const isDeviceError =
246+
cameraError instanceof DOMException &&
247+
[
248+
"NotFoundError",
249+
"DevicesNotFoundError",
250+
"OverconstrainedError",
251+
"NotReadableError",
252+
].includes(cameraError.name);
253+
toast.error(t(isDeviceError ? "recording.cameraNotFound" : "recording.cameraBlocked"));
254+
webcamReady.current = true;
255+
}
256+
}
257+
};
258+
259+
void acquire();
260+
261+
return () => {
262+
cancelled = true;
263+
webcamReady.current = false;
264+
if (acquiredStream) {
265+
acquiredStream.getTracks().forEach((track) => {
266+
track.onended = null;
267+
track.stop();
268+
});
269+
webcamStream.current = null;
270+
}
271+
};
272+
}, [webcamEnabled, webcamDeviceId, t]);
273+
197274
const finalizeRecording = useCallback(
198275
(
199276
activeScreenRecorder: RecorderHandle,
@@ -438,30 +515,23 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
438515
}
439516

440517
if (webcamEnabled) {
441-
try {
442-
webcamStream.current = await navigator.mediaDevices.getUserMedia({
443-
audio: false,
444-
video: webcamDeviceId
445-
? {
446-
deviceId: { exact: webcamDeviceId },
447-
width: { ideal: WEBCAM_TARGET_WIDTH },
448-
height: { ideal: WEBCAM_TARGET_HEIGHT },
449-
frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE },
450-
}
451-
: {
452-
width: { ideal: WEBCAM_TARGET_WIDTH },
453-
height: { ideal: WEBCAM_TARGET_HEIGHT },
454-
frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE },
455-
},
518+
if (!webcamReady.current) {
519+
await new Promise<void>((resolve) => {
520+
const interval = setInterval(() => {
521+
if (webcamReady.current) {
522+
clearInterval(interval);
523+
resolve();
524+
}
525+
}, 50);
526+
setTimeout(() => {
527+
clearInterval(interval);
528+
resolve();
529+
}, 5000);
456530
});
457-
} catch (cameraError) {
458-
console.warn("Failed to get webcam access:", cameraError);
459-
if (webcamStream.current) {
460-
webcamStream.current.getTracks().forEach((track) => track.stop());
461-
webcamStream.current = null;
462-
}
531+
}
532+
if (!webcamStream.current) {
533+
webcamAcquireId.current++;
463534
setWebcamEnabledState(false);
464-
toast.error(t("recording.cameraDenied"));
465535
}
466536
}
467537

src/i18n/locales/en/editor.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
"systemAudioUnavailable": "System audio not available. Recording without system audio.",
3737
"microphoneDenied": "Microphone access denied. Recording will continue without audio.",
3838
"cameraDenied": "Camera access denied. Recording will continue without webcam.",
39+
"cameraDisconnected": "Webcam disconnected.",
40+
"cameraNotFound": "Camera not found.",
3941
"permissionDenied": "Recording permission denied. Please allow screen recording."
4042
}
4143
}

src/i18n/locales/es/editor.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"systemAudioUnavailable": "Audio del sistema no disponible. Grabando sin audio del sistema.",
3131
"microphoneDenied": "Acceso al micrófono denegado. La grabación continuará sin audio.",
3232
"cameraDenied": "Acceso a la cámara denegado. La grabación continuará sin cámara web.",
33+
"cameraDisconnected": "Cámara web desconectada.",
34+
"cameraNotFound": "Cámara no encontrada.",
3335
"permissionDenied": "Permiso de grabación denegado. Por favor permite la grabación de pantalla."
3436
}
3537
}

src/i18n/locales/zh-CN/editor.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
"systemAudioUnavailable": "系统音频不可用。将在无系统音频的情况下录制。",
3737
"microphoneDenied": "麦克风权限被拒绝。录制将继续,但不包含音频。",
3838
"cameraDenied": "摄像头权限被拒绝。录制将继续,但不包含摄像头画面。",
39+
"cameraDisconnected": "摄像头已断开连接。",
40+
"cameraNotFound": "未找到摄像头。",
3941
"permissionDenied": "录屏权限被拒绝。请允许屏幕录制。"
4042
}
4143
}

src/lib/requestCameraAccess.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ export async function requestCameraAccess(): Promise<CameraAccessResult> {
1717
if (window.electronAPI?.requestCameraAccess) {
1818
try {
1919
const electronResult = await window.electronAPI.requestCameraAccess();
20-
if (!electronResult.success || !electronResult.granted) {
21-
return electronResult;
22-
}
20+
return electronResult;
2321
} catch (error) {
2422
return {
2523
success: false,

0 commit comments

Comments
 (0)