diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index f1b66b8..8fd8934 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -436,6 +436,7 @@ export function LaunchWindow() { onClick={async () => { await setWebcamEnabled(!webcamEnabled); }} + disabled={recording} title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")} > {webcamEnabled diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 0c418c1..ba95e60 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -145,10 +145,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { microphoneStream.current.getTracks().forEach((track) => track.stop()); microphoneStream.current = null; } - if (webcamStream.current) { - webcamStream.current.getTracks().forEach((track) => track.stop()); - webcamStream.current = null; - } if (mixingContext.current) { mixingContext.current.close().catch(() => { // Ignore close errors during recorder teardown. @@ -181,6 +177,66 @@ export function useScreenRecorder(): UseScreenRecorderReturn { [t], ); + useEffect(() => { + if (!webcamEnabled) return; + + let cancelled = false; + let acquiredStream: MediaStream | null = null; + + const acquire = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: false, + video: webcamDeviceId + ? { + deviceId: { exact: webcamDeviceId }, + width: { ideal: WEBCAM_TARGET_WIDTH }, + height: { ideal: WEBCAM_TARGET_HEIGHT }, + frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE }, + } + : { + width: { ideal: WEBCAM_TARGET_WIDTH }, + height: { ideal: WEBCAM_TARGET_HEIGHT }, + frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE }, + }, + }); + + if (cancelled) { + stream.getTracks().forEach((track) => track.stop()); + return; + } + + acquiredStream = stream; + stream.getVideoTracks().forEach((track) => { + track.onended = () => { + webcamStream.current = null; + if (!restarting.current) { + setWebcamEnabledState(false); + toast.error(t("recording.cameraDisconnected")); + } + }; + }); + webcamStream.current = stream; + } catch (cameraError) { + if (!cancelled) { + console.warn("Failed to get webcam access:", cameraError); + setWebcamEnabledState(false); + toast.error(t("recording.cameraBlocked")); + } + } + }; + + void acquire(); + + return () => { + cancelled = true; + if (acquiredStream) { + acquiredStream.getTracks().forEach((track) => track.stop()); + webcamStream.current = null; + } + }; + }, [webcamEnabled, webcamDeviceId, t]); + const finalizeRecording = useCallback( ( activeScreenRecorder: RecorderHandle, @@ -408,32 +464,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } - if (webcamEnabled) { - try { - webcamStream.current = await navigator.mediaDevices.getUserMedia({ - audio: false, - video: webcamDeviceId - ? { - deviceId: { exact: webcamDeviceId }, - width: { ideal: WEBCAM_TARGET_WIDTH }, - height: { ideal: WEBCAM_TARGET_HEIGHT }, - frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE }, - } - : { - width: { ideal: WEBCAM_TARGET_WIDTH }, - height: { ideal: WEBCAM_TARGET_HEIGHT }, - frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE }, - }, - }); - } catch (cameraError) { - console.warn("Failed to get webcam access:", cameraError); - if (webcamStream.current) { - webcamStream.current.getTracks().forEach((track) => track.stop()); - webcamStream.current = null; - } - setWebcamEnabledState(false); - toast.error(t("recording.cameraDenied")); - } + if (webcamEnabled && !webcamStream.current) { + setWebcamEnabledState(false); + toast.error(t("recording.cameraDenied")); } stream.current = new MediaStream(); diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json index 6fdc310..8acd181 100644 --- a/src/i18n/locales/en/editor.json +++ b/src/i18n/locales/en/editor.json @@ -30,6 +30,7 @@ "systemAudioUnavailable": "System audio not available. Recording without system audio.", "microphoneDenied": "Microphone access denied. Recording will continue without audio.", "cameraDenied": "Camera access denied. Recording will continue without webcam.", + "cameraDisconnected": "Webcam disconnected.", "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 99adc78..5834622 100644 --- a/src/i18n/locales/es/editor.json +++ b/src/i18n/locales/es/editor.json @@ -30,6 +30,7 @@ "systemAudioUnavailable": "Audio del sistema no disponible. Grabando sin audio del sistema.", "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.", "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 5d27bef..0723c88 100644 --- a/src/i18n/locales/zh-CN/editor.json +++ b/src/i18n/locales/zh-CN/editor.json @@ -30,6 +30,7 @@ "systemAudioUnavailable": "系统音频不可用。将在无系统音频的情况下录制。", "microphoneDenied": "麦克风权限被拒绝。录制将继续,但不包含音频。", "cameraDenied": "摄像头权限被拒绝。录制将继续,但不包含摄像头画面。", + "cameraDisconnected": "摄像头已断开连接。", "permissionDenied": "录屏权限被拒绝。请允许屏幕录制。" } } diff --git a/src/lib/requestCameraAccess.ts b/src/lib/requestCameraAccess.ts index 2494224..cfcd4d1 100644 --- a/src/lib/requestCameraAccess.ts +++ b/src/lib/requestCameraAccess.ts @@ -17,9 +17,7 @@ export async function requestCameraAccess(): Promise { if (window.electronAPI?.requestCameraAccess) { try { const electronResult = await window.electronAPI.requestCameraAccess(); - if (!electronResult.success || !electronResult.granted) { - return electronResult; - } + return electronResult; } catch (error) { return { success: false,