diff --git a/.gitignore b/.gitignore index 70cc387..199ff44 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ dist-ssr *.sw? release/** *.kiro/ +.claude/ # npx electron-builder --mac --win # Playwright diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index ba95e60..8448b82 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -103,6 +103,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const allowAutoFinalize = useRef(false); const discardRecordingId = useRef(null); const restarting = useRef(false); + const webcamReady = useRef(false); const selectMimeType = () => { const preferred = [ @@ -182,6 +183,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { let cancelled = false; let acquiredStream: MediaStream | null = null; + webcamReady.current = false; const acquire = async () => { try { @@ -217,11 +219,21 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }; }); webcamStream.current = stream; + webcamReady.current = true; } catch (cameraError) { if (!cancelled) { console.warn("Failed to get webcam access:", cameraError); setWebcamEnabledState(false); - toast.error(t("recording.cameraBlocked")); + const isDeviceError = + cameraError instanceof DOMException && + [ + "NotFoundError", + "DevicesNotFoundError", + "OverconstrainedError", + "NotReadableError", + ].includes(cameraError.name); + toast.error(t(isDeviceError ? "recording.cameraNotFound" : "recording.cameraBlocked")); + webcamReady.current = true; } } }; @@ -230,6 +242,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return () => { cancelled = true; + webcamReady.current = false; if (acquiredStream) { acquiredStream.getTracks().forEach((track) => track.stop()); webcamStream.current = null; @@ -464,9 +477,26 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } - if (webcamEnabled && !webcamStream.current) { - setWebcamEnabledState(false); - toast.error(t("recording.cameraDenied")); + if (webcamEnabled) { + if (!webcamReady.current) { + await new Promise((resolve) => { + const interval = setInterval(() => { + if (webcamReady.current) { + clearInterval(interval); + resolve(); + } + }, 50); + setTimeout(() => { + clearInterval(interval); + resolve(); + }, 5000); + }); + } + if (!webcamStream.current) { + // The useEffect already showed the appropriate error toast + // (cameraNotFound or cameraBlocked), so just disable the state. + setWebcamEnabledState(false); + } } stream.current = new MediaStream(); diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json index 8acd181..e0cf4fd 100644 --- a/src/i18n/locales/en/editor.json +++ b/src/i18n/locales/en/editor.json @@ -31,6 +31,7 @@ "microphoneDenied": "Microphone access denied. Recording will continue without audio.", "cameraDenied": "Camera access denied. Recording will continue without webcam.", "cameraDisconnected": "Webcam disconnected.", + "cameraNotFound": "Camera not found.", "permissionDenied": "Recording permission denied. Please allow screen recording." } } diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json index 5834622..7956b75 100644 --- a/src/i18n/locales/es/editor.json +++ b/src/i18n/locales/es/editor.json @@ -31,6 +31,7 @@ "microphoneDenied": "Acceso al micrófono denegado. La grabación continuará sin audio.", "cameraDenied": "Acceso a la cámara denegado. La grabación continuará sin cámara web.", "cameraDisconnected": "Cámara web desconectada.", + "cameraNotFound": "Cámara no encontrada.", "permissionDenied": "Permiso de grabación denegado. Por favor permite la grabación de pantalla." } } diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json index 0723c88..2360fe8 100644 --- a/src/i18n/locales/zh-CN/editor.json +++ b/src/i18n/locales/zh-CN/editor.json @@ -31,6 +31,7 @@ "microphoneDenied": "麦克风权限被拒绝。录制将继续,但不包含音频。", "cameraDenied": "摄像头权限被拒绝。录制将继续,但不包含摄像头画面。", "cameraDisconnected": "摄像头已断开连接。", + "cameraNotFound": "未找到摄像头。", "permissionDenied": "录屏权限被拒绝。请允许屏幕录制。" } }