Merge branch 'siddharthvaddem:main' into main

This commit is contained in:
Ayush Mukherjee
2026-04-03 18:58:00 +05:30
committed by GitHub
11 changed files with 1281 additions and 173 deletions
+292 -155
View File
@@ -21,6 +21,7 @@ import { type Locale, SUPPORTED_LOCALES } from "@/i18n/config";
import { getLocaleName } from "@/i18n/loader";
import { isMac as getIsMac } from "@/utils/platformUtils";
import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter";
import { useCameraDevices } from "../../hooks/useCameraDevices";
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
import { useScreenRecorder } from "../../hooks/useScreenRecorder";
import { requestCameraAccess } from "../../lib/requestCameraAccess";
@@ -86,23 +87,64 @@ export function LaunchWindow() {
setSystemAudioEnabled,
webcamEnabled,
setWebcamEnabled,
webcamDeviceId,
setWebcamDeviceId,
} = useScreenRecorder();
const [recordingStart, setRecordingStart] = useState<number | null>(null);
const [elapsed, setElapsed] = useState(0);
const showMicControls = microphoneEnabled && !recording;
const { devices, selectedDeviceId, setSelectedDeviceId } =
useMicrophoneDevices(microphoneEnabled);
const showWebcamControls = webcamEnabled && !recording;
const [isMicHovered, setIsMicHovered] = useState(false);
const [isMicFocused, setIsMicFocused] = useState(false);
const micExpanded = isMicHovered || isMicFocused;
const [isWebcamHovered, setIsWebcamHovered] = useState(false);
const [isWebcamFocused, setIsWebcamFocused] = useState(false);
const webcamExpanded = isWebcamHovered || isWebcamFocused;
const {
devices: micDevices,
selectedDeviceId: selectedMicId,
setSelectedDeviceId: setSelectedMicId,
} = useMicrophoneDevices(microphoneEnabled);
const {
devices: cameraDevices,
selectedDeviceId: selectedCameraId,
setSelectedDeviceId: setSelectedCameraId,
isLoading: isCameraDevicesLoading,
error: cameraDevicesError,
} = useCameraDevices(webcamEnabled);
const selectedMicLabel =
micDevices.find((d) => d.deviceId === (microphoneDeviceId || selectedMicId))?.label ||
t("audio.defaultMicrophone");
const selectedCameraLabel = isCameraDevicesLoading
? t("webcam.searching")
: cameraDevicesError
? t("webcam.unavailable")
: cameraDevices.length === 0
? t("webcam.noneFound")
: cameraDevices.find((d) => d.deviceId === (webcamDeviceId || selectedCameraId))?.label ||
t("webcam.defaultCamera");
const { level } = useAudioLevelMeter({
enabled: showMicControls,
deviceId: microphoneDeviceId,
});
useEffect(() => {
if (selectedDeviceId && selectedDeviceId !== "default") {
setMicrophoneDeviceId(selectedDeviceId);
if (selectedMicId && selectedMicId !== "default") {
setMicrophoneDeviceId(selectedMicId);
}
}, [selectedDeviceId, setMicrophoneDeviceId]);
}, [selectedMicId, setMicrophoneDeviceId]);
useEffect(() => {
if (selectedCameraId) {
setWebcamDeviceId(selectedCameraId);
}
}, [selectedCameraId, setWebcamDeviceId]);
useEffect(() => {
let timer: NodeJS.Timeout | null = null;
@@ -199,10 +241,10 @@ export function LaunchWindow() {
};
return (
<div className="w-full h-full flex items-end justify-center bg-transparent relative">
<div className={`w-screen h-screen bg-transparent ${styles.electronDrag}`}>
{/* Language switcher — top-left, beside traffic lights */}
<div
className={`absolute top-2 flex items-center gap-1 px-2 py-1 rounded-md text-white/50 hover:text-white/90 hover:bg-white/10 transition-all duration-150 ${isMac ? "left-[72px]" : "left-2"} ${styles.electronNoDrag}`}
className={`fixed top-2 flex items-center gap-1 px-2 py-1 rounded-md text-white/50 hover:text-white/90 hover:bg-white/10 transition-all duration-150 ${isMac ? "left-[72px]" : "left-2"} ${styles.electronNoDrag}`}
>
<Languages size={14} />
<select
@@ -219,165 +261,260 @@ export function LaunchWindow() {
</select>
</div>
<div className={`flex flex-col items-center gap-2 mx-auto ${styles.electronDrag}`}>
{/* Mic controls panel */}
{showMicControls && (
<div
className={`flex items-center gap-2 px-4 py-2 bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[16px] backdrop-saturate-[140%] border border-[rgba(80,80,120,0.25)] rounded-2xl shadow-mic-panel animate-mic-panel-in ${styles.electronNoDrag}`}
>
<div className="relative flex-1" style={{ maxWidth: "70%" }}>
<select
value={microphoneDeviceId || selectedDeviceId}
onChange={(e) => {
setSelectedDeviceId(e.target.value);
setMicrophoneDeviceId(e.target.value);
}}
className="w-full appearance-none bg-white/10 text-white text-xs rounded-full pl-3 pr-7 py-2 border border-white/20 outline-none truncate"
>
{devices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</select>
<ChevronDown
size={14}
className="absolute right-2 top-1/2 -translate-y-1/2 text-white/60 pointer-events-none"
{/* Device selectors — fixed above HUD bar, viewport-relative, never clipped */}
{(showMicControls || showWebcamControls) && (
<div
className={`fixed bottom-[60px] left-1/2 -translate-x-1/2 flex items-center gap-2 animate-mic-panel-in ${styles.electronNoDrag}`}
>
{/* Mic selector */}
{showMicControls && (
<div
className={`flex items-center gap-2 px-3 py-1.5 h-[36px] bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[24px] border border-white/10 rounded-xl shadow-2xl transition-all duration-300 overflow-hidden ${!micExpanded ? "opacity-60 grayscale-[0.5]" : "opacity-100"}`}
onMouseEnter={() => setIsMicHovered(true)}
onMouseLeave={() => setIsMicHovered(false)}
onFocus={() => setIsMicFocused(true)}
onBlur={() => setIsMicFocused(false)}
style={{ width: micExpanded ? "240px" : "140px", transition: "width 300ms ease" }}
>
<div className="relative flex-1 min-w-0">
{!micExpanded && (
<div className="text-white/60 text-[10px] font-medium truncate">
{selectedMicLabel}
</div>
)}
<select
value={microphoneDeviceId || selectedMicId}
onChange={(e) => {
setSelectedMicId(e.target.value);
setMicrophoneDeviceId(e.target.value);
}}
className={`w-full appearance-none bg-white/5 text-white text-[11px] rounded-lg pl-2 pr-6 py-1 border border-white/10 outline-none hover:bg-white/10 transition-colors cursor-pointer ${!micExpanded ? "sr-only" : ""}`}
>
{micDevices.map((device) => (
<option key={device.deviceId} value={device.deviceId} className="bg-[#1c1c24]">
{device.label}
</option>
))}
</select>
{micExpanded && (
<ChevronDown
size={12}
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-white/40 pointer-events-none"
/>
)}
</div>
<AudioLevelMeter
level={level}
className={`${micExpanded ? "w-16" : "w-8"} h-2 transition-all duration-300`}
/>
</div>
<AudioLevelMeter level={level} className="w-24 h-4" />
</div>
)}
{/* Main pill bar */}
<div className="flex items-center gap-1.5 px-2 py-1.5 isolate rounded-full shadow-hud-bar bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[16px] backdrop-saturate-[140%] border border-[rgba(80,80,120,0.25)]">
{/* Drag handle */}
<div className={`flex items-center px-1 ${styles.electronDrag}`}>
{getIcon("drag", "text-white/30")}
</div>
{/* Source selector */}
<button
className={`${hudGroupClasses} p-2 ${styles.electronNoDrag}`}
onClick={openSourceSelector}
disabled={recording}
title={selectedSource}
>
{getIcon("monitor", "text-white/80")}
<span className="text-white/70 text-[11px] max-w-[72px] truncate">
{selectedSource}
</span>
</button>
{/* Audio controls group */}
<div className={`${hudGroupClasses} ${styles.electronNoDrag}`}>
<button
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
disabled={recording}
title={
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
}
>
{systemAudioEnabled
? getIcon("volumeOn", "text-green-400")
: getIcon("volumeOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={toggleMicrophone}
disabled={recording}
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
>
{microphoneEnabled
? getIcon("micOn", "text-green-400")
: getIcon("micOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={async () => {
await setWebcamEnabled(!webcamEnabled);
}}
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
>
{webcamEnabled
? getIcon("webcamOn", "text-green-400")
: getIcon("webcamOff", "text-white/40")}
</button>
</div>
{/* Record/Stop group */}
<button
className={`flex items-center gap-0.5 rounded-full p-2 transition-colors duration-150 ${styles.electronNoDrag} ${
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
}`}
onClick={hasSelectedSource ? toggleRecording : openSourceSelector}
disabled={!hasSelectedSource && !recording}
style={{ flex: "0 0 auto" }}
>
{recording ? (
<>
{getIcon("stop", "text-red-400")}
<span className="text-red-400 text-xs font-semibold tabular-nums">
{formatTimePadded(elapsed)}
</span>
</>
) : (
getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")
)}
</button>
{/* Restart recording */}
{recording && (
<Tooltip content={t("tooltips.restartRecording")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={restartRecording}
>
{getIcon("restart", "text-white/60")}
</button>
</Tooltip>
)}
{/* Open video file */}
<Tooltip content={t("tooltips.openVideoFile")}>
{/* Webcam selector */}
{showWebcamControls && (
<div
className={`flex items-center gap-2 px-3 py-1.5 h-[36px] bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[24px] border border-white/10 rounded-xl shadow-2xl transition-all duration-300 overflow-hidden ${!webcamExpanded ? "opacity-60 grayscale-[0.5]" : "opacity-100"}`}
onMouseEnter={() => setIsWebcamHovered(true)}
onMouseLeave={() => setIsWebcamHovered(false)}
onFocus={() => setIsWebcamFocused(true)}
onBlur={() => setIsWebcamFocused(false)}
style={{ width: webcamExpanded ? "240px" : "140px", transition: "width 300ms ease" }}
>
<div className="relative flex-1 min-w-0">
{!webcamExpanded && (
<div className="text-white/60 text-[10px] font-medium truncate">
{selectedCameraLabel}
</div>
)}
{webcamExpanded &&
(isCameraDevicesLoading ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.searching")}
</span>
) : cameraDevicesError ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.unavailable")}
</span>
) : cameraDevices.length === 0 ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.noneFound")}
</span>
) : (
<>
<select
value={webcamDeviceId || selectedCameraId}
onChange={(e) => {
setSelectedCameraId(e.target.value);
setWebcamDeviceId(e.target.value);
}}
className="w-full appearance-none bg-white/5 text-white text-[11px] rounded-lg pl-2 pr-6 py-1 border border-white/10 outline-none hover:bg-white/10 transition-colors cursor-pointer"
>
{cameraDevices.map((device) => (
<option
key={device.deviceId}
value={device.deviceId}
className="bg-[#1c1c24]"
>
{device.label}
</option>
))}
</select>
<ChevronDown
size={12}
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-white/40 pointer-events-none"
/>
</>
))}
{(!webcamExpanded || cameraDevices.length === 0) && (
<select
value={webcamDeviceId || selectedCameraId}
onChange={(e) => {
setSelectedCameraId(e.target.value);
setWebcamDeviceId(e.target.value);
}}
className="sr-only"
>
{cameraDevices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</select>
)}
</div>
</div>
)}
</div>
)}
{/* HUD bar — fixed at bottom center, viewport-relative, never moves */}
<div
className={`fixed bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-1.5 px-2 py-1.5 rounded-full shadow-hud-bar bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[16px] backdrop-saturate-[140%] border border-[rgba(80,80,120,0.25)]`}
>
{/* Drag handle */}
<div className={`flex items-center px-1 ${styles.electronDrag}`}>
{getIcon("drag", "text-white/30")}
</div>
{/* Source selector */}
<button
className={`${hudGroupClasses} p-2 ${styles.electronNoDrag}`}
onClick={openSourceSelector}
disabled={recording}
title={selectedSource}
>
{getIcon("monitor", "text-white/80")}
<span className="text-white/70 text-[11px] max-w-[72px] truncate">{selectedSource}</span>
</button>
{/* Audio controls group */}
<div className={`${hudGroupClasses} ${styles.electronNoDrag}`}>
<button
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
disabled={recording}
title={
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
}
>
{systemAudioEnabled
? getIcon("volumeOn", "text-green-400")
: getIcon("volumeOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={toggleMicrophone}
disabled={recording}
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
>
{microphoneEnabled
? getIcon("micOn", "text-green-400")
: getIcon("micOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={async () => {
await setWebcamEnabled(!webcamEnabled);
}}
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
>
{webcamEnabled
? getIcon("webcamOn", "text-green-400")
: getIcon("webcamOff", "text-white/40")}
</button>
</div>
{/* Record/Stop group */}
<button
className={`flex items-center gap-0.5 rounded-full p-2 transition-colors duration-150 ${styles.electronNoDrag} ${
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
}`}
onClick={toggleRecording}
disabled={!hasSelectedSource && !recording}
style={{ flex: "0 0 auto" }}
>
{recording ? (
<>
{getIcon("stop", "text-red-400")}
<span className="text-red-400 text-xs font-semibold tabular-nums">
{formatTimePadded(elapsed)}
</span>
</>
) : (
getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")
)}
</button>
{/* Restart recording */}
{recording && (
<Tooltip content={t("tooltips.restartRecording")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={openVideoFile}
disabled={recording}
onClick={restartRecording}
>
{getIcon("videoFile", "text-white/60")}
{getIcon("restart", "text-white/60")}
</button>
</Tooltip>
)}
{/* Open project */}
<Tooltip content={t("tooltips.openProject")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={openProjectFile}
disabled={recording}
>
{getIcon("folder", "text-white/60")}
</button>
</Tooltip>
{/* Open video file */}
<Tooltip content={t("tooltips.openVideoFile")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={openVideoFile}
disabled={recording}
>
{getIcon("videoFile", "text-white/60")}
</button>
</Tooltip>
{/* Window controls */}
<div className={`flex items-center gap-0.5 ${styles.electronNoDrag}`}>
<button
className={windowBtnClasses}
title={t("tooltips.hideHUD")}
onClick={sendHudOverlayHide}
>
{getIcon("minimize", "text-white")}
</button>
<button
className={windowBtnClasses}
title={t("tooltips.closeApp")}
onClick={sendHudOverlayClose}
>
{getIcon("close", "text-white")}
</button>
</div>
{/* Open project */}
<Tooltip content={t("tooltips.openProject")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={openProjectFile}
disabled={recording}
>
{getIcon("folder", "text-white/60")}
</button>
</Tooltip>
{/* Window controls */}
<div className={`flex items-center gap-0.5 ${styles.electronNoDrag}`}>
<button
className={windowBtnClasses}
title={t("tooltips.hideHUD")}
onClick={sendHudOverlayHide}
>
{getIcon("minimize", "text-white")}
</button>
<button
className={windowBtnClasses}
title={t("tooltips.closeApp")}
onClick={sendHudOverlayClose}
>
{getIcon("close", "text-white")}
</button>
</div>
</div>
</div>
+113
View File
@@ -0,0 +1,113 @@
import { act, renderHook, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useCameraDevices } from "./useCameraDevices";
// Mock navigator.mediaDevices
const mockDevices = [
{ kind: "videoinput", deviceId: "cam1", label: "Camera 1", groupId: "group1" },
{ kind: "videoinput", deviceId: "cam2", label: "Camera 2", groupId: "group1" },
{ kind: "audioinput", deviceId: "mic1", label: "Mic 1", groupId: "group2" },
];
const mockGetUserMedia = vi.fn().mockResolvedValue({
getTracks: () => [{ stop: vi.fn() }],
});
const mockEnumerateDevices = vi.fn().mockResolvedValue(mockDevices);
Object.defineProperty(global.navigator, "mediaDevices", {
value: {
enumerateDevices: mockEnumerateDevices,
getUserMedia: mockGetUserMedia,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
},
configurable: true,
});
describe("useCameraDevices", () => {
beforeEach(() => {
vi.clearAllMocks();
mockEnumerateDevices.mockResolvedValue(mockDevices);
mockGetUserMedia.mockResolvedValue({
getTracks: () => [{ stop: vi.fn() }],
});
});
afterEach(() => {
vi.resetAllMocks();
});
it("should list video input devices", async () => {
const { result } = renderHook(() => useCameraDevices(true));
await waitFor(() => {
expect(result.current.devices).toHaveLength(2);
});
expect(result.current.devices[0].label).toBe("Camera 1");
expect(result.current.devices[1].deviceId).toBe("cam2");
});
it("should set first device as default", async () => {
const { result } = renderHook(() => useCameraDevices(true));
await waitFor(() => {
expect(result.current.selectedDeviceId).toBe("cam1");
});
});
it("should use device ID as fallback label when label is missing", async () => {
mockEnumerateDevices.mockResolvedValueOnce([
{ kind: "videoinput", deviceId: "cam1abc123456", label: "", groupId: "group1" },
]);
const { result } = renderHook(() => useCameraDevices(true));
await waitFor(() => {
expect(result.current.devices[0]?.label).toBe("Camera cam1abc1");
});
expect(mockGetUserMedia).not.toHaveBeenCalled();
});
it("should set error state when enumeration fails", async () => {
mockEnumerateDevices.mockRejectedValueOnce(new Error("Permission denied"));
const { result } = renderHook(() => useCameraDevices(true));
await waitFor(() => {
expect(result.current.error).toBe("Permission denied");
});
expect(result.current.devices).toHaveLength(0);
expect(result.current.isLoading).toBe(false);
});
it("should fall back to first available device when selected device is unplugged", async () => {
const { result } = renderHook(() => useCameraDevices(true));
await waitFor(() => {
expect(result.current.selectedDeviceId).toBe("cam1");
});
// Simulate cam1 being unplugged — only cam2 remains
const cam2Only = [
{ kind: "videoinput", deviceId: "cam2", label: "Camera 2", groupId: "group1" },
];
mockEnumerateDevices.mockResolvedValueOnce(cam2Only);
// Trigger devicechange event via the registered handler
const devicechangeHandler = (
navigator.mediaDevices.addEventListener as ReturnType<typeof vi.fn>
).mock.calls[0]?.[1] as (() => void) | undefined;
await act(async () => {
devicechangeHandler?.();
});
await waitFor(() => {
expect(result.current.selectedDeviceId).toBe("cam2");
});
});
});
+64
View File
@@ -0,0 +1,64 @@
import { useEffect, useRef, useState } from "react";
export interface CameraDevice {
deviceId: string;
label: string;
groupId: string;
}
export function useCameraDevices(enabled: boolean = false) {
const [devices, setDevices] = useState<CameraDevice[]>([]);
const [selectedDeviceId, setSelectedDeviceId] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const selectedDeviceIdRef = useRef(selectedDeviceId);
selectedDeviceIdRef.current = selectedDeviceId;
useEffect(() => {
if (!enabled) return;
let mounted = true;
const loadDevices = async () => {
try {
setIsLoading(true);
setError(null);
// Enumerate without requesting a second stream — the recorder handles
// the real acquisition; unlabeled devices fall back to their device ID.
const allDevices = await navigator.mediaDevices.enumerateDevices();
const videoInputs = allDevices
.filter((device) => device.kind === "videoinput")
.map((device) => ({
deviceId: device.deviceId,
label: device.label || `Camera ${device.deviceId.slice(0, 8)}`,
groupId: device.groupId,
}));
if (mounted) {
setDevices(videoInputs);
const currentId = selectedDeviceIdRef.current;
const stillAvailable = videoInputs.some((d) => d.deviceId === currentId);
if (!currentId || !stillAvailable) {
setSelectedDeviceId(videoInputs[0]?.deviceId ?? "");
}
setIsLoading(false);
}
} catch (err) {
if (mounted) {
setError(err instanceof Error ? err.message : "Failed to load cameras");
setIsLoading(false);
}
}
};
loadDevices();
navigator.mediaDevices.addEventListener("devicechange", loadDevices);
return () => {
mounted = false;
navigator.mediaDevices.removeEventListener("devicechange", loadDevices);
};
}, [enabled]);
return { devices, selectedDeviceId, setSelectedDeviceId, isLoading, error };
}
+18 -5
View File
@@ -47,6 +47,8 @@ type UseScreenRecorderReturn = {
setMicrophoneEnabled: (enabled: boolean) => void;
microphoneDeviceId: string | undefined;
setMicrophoneDeviceId: (deviceId: string | undefined) => void;
webcamDeviceId: string | undefined;
setWebcamDeviceId: (deviceId: string | undefined) => void;
systemAudioEnabled: boolean;
setSystemAudioEnabled: (enabled: boolean) => void;
webcamEnabled: boolean;
@@ -85,6 +87,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const [recording, setRecording] = useState(false);
const [microphoneEnabled, setMicrophoneEnabled] = useState(false);
const [microphoneDeviceId, setMicrophoneDeviceId] = useState<string | undefined>(undefined);
const [webcamDeviceId, setWebcamDeviceId] = useState<string | undefined>(undefined);
const [systemAudioEnabled, setSystemAudioEnabled] = useState(false);
const [webcamEnabled, setWebcamEnabledState] = useState(false);
const screenRecorder = useRef<RecorderHandle | null>(null);
@@ -409,11 +412,18 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
try {
webcamStream.current = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
width: { ideal: WEBCAM_TARGET_WIDTH },
height: { ideal: WEBCAM_TARGET_HEIGHT },
frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE },
},
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);
@@ -563,6 +573,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
restarting.current = true;
discardRecordingId.current = activeRecordingId;
allowAutoFinalize.current = false;
const stopPromises = [
new Promise<void>((resolve) => {
@@ -598,6 +609,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
setMicrophoneEnabled,
microphoneDeviceId,
setMicrophoneDeviceId,
webcamDeviceId,
setWebcamDeviceId,
systemAudioEnabled,
setSystemAudioEnabled,
webcamEnabled,
+7 -2
View File
@@ -10,11 +10,16 @@
"enableSystemAudio": "Enable system audio",
"disableSystemAudio": "Disable system audio",
"enableMicrophone": "Enable microphone",
"disableMicrophone": "Disable microphone"
"disableMicrophone": "Disable microphone",
"defaultMicrophone": "Default Microphone"
},
"webcam": {
"enableWebcam": "Enable webcam",
"disableWebcam": "Disable webcam"
"disableWebcam": "Disable webcam",
"defaultCamera": "Default Camera",
"searching": "Searching...",
"noneFound": "No camera found",
"unavailable": "Camera unavailable"
},
"sourceSelector": {
"loading": "Loading sources...",
+7 -2
View File
@@ -10,11 +10,16 @@
"enableSystemAudio": "Activar audio del sistema",
"disableSystemAudio": "Desactivar audio del sistema",
"enableMicrophone": "Activar micrófono",
"disableMicrophone": "Desactivar micrófono"
"disableMicrophone": "Desactivar micrófono",
"defaultMicrophone": "Micrófono predeterminado"
},
"webcam": {
"enableWebcam": "Activar cámara web",
"disableWebcam": "Desactivar cámara web"
"disableWebcam": "Desactivar cámara web",
"defaultCamera": "Cámara predeterminada",
"searching": "Buscando...",
"noneFound": "No se encontró cámara",
"unavailable": "Cámara no disponible"
},
"sourceSelector": {
"loading": "Cargando fuentes...",
+7 -2
View File
@@ -10,11 +10,16 @@
"enableSystemAudio": "启用系统音频",
"disableSystemAudio": "禁用系统音频",
"enableMicrophone": "启用麦克风",
"disableMicrophone": "禁用麦克风"
"disableMicrophone": "禁用麦克风",
"defaultMicrophone": "默认麦克风"
},
"webcam": {
"enableWebcam": "启用摄像头",
"disableWebcam": "禁用摄像头"
"disableWebcam": "禁用摄像头",
"defaultCamera": "默认摄像头",
"searching": "正在搜索...",
"noneFound": "未找到摄像头",
"unavailable": "摄像头不可用"
},
"sourceSelector": {
"loading": "正在加载源...",