diff --git a/js/src/features/settings/audio-video-settings-page.tsx b/js/src/features/settings/audio-video-settings-page.tsx index 4e8bd15..9fbdf4b 100644 --- a/js/src/features/settings/audio-video-settings-page.tsx +++ b/js/src/features/settings/audio-video-settings-page.tsx @@ -1,10 +1,9 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { ArrowLeft, Camera, Mic, VideoOff } from "lucide-react"; +import { ArrowLeft, VideoOff } from "lucide-react"; import { WindowControls } from "@/components/window-controls"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; import { Muted } from "@/components/ui/typography"; import { Select, @@ -27,12 +26,6 @@ import { const SYSTEM_DEFAULT = "__system_default__"; -/** - * Owns the preview MediaStream for the settings page. Rebuilds the - * stream whenever the effective mic or camera deviceId changes, and - * always stops the previous tracks before issuing a new request so - * the OS camera indicator doesn't linger. - */ function usePreviewStream( enabled: boolean, micId: string | undefined, @@ -56,8 +49,8 @@ function usePreviewStream( : true; const video: MediaTrackConstraints | false = cameraAvailable ? cameraId - ? { deviceId: { exact: cameraId }, aspectRatio: { ideal: 16 / 10 } } - : { aspectRatio: { ideal: 16 / 10 } } + ? { deviceId: { exact: cameraId }, aspectRatio: { ideal: 16 / 9 } } + : { aspectRatio: { ideal: 16 / 9 } } : false; navigator.mediaDevices @@ -74,9 +67,7 @@ function usePreviewStream( .catch((err: unknown) => { if (cancelled) return; setStream(null); - setError( - err instanceof Error ? err.message : "Unable to access devices", - ); + setError(err instanceof Error ? err.message : "Unable to access devices"); }); return () => { @@ -94,94 +85,36 @@ function deviceLabel(d: MediaDeviceInfo, index: number): string { return `${kind} ${index + 1}`; } -function MicSection({ - devices, - saved, - onChange, - stream, - unavailable, -}: { - devices: MediaDeviceInfo[]; - saved: SavedDevice | null; - onChange: (device: SavedDevice | null) => void; - stream: MediaStream | null; - unavailable: boolean; -}) { - const audioSource = useAudioSource(stream); - const value = saved?.deviceId ?? SYSTEM_DEFAULT; - +function FieldLabel({ children }: { children: React.ReactNode }) { return ( -
-
- -

Microphone

-
- - -
-
- {audioSource ? ( - - ) : ( -
- {[0, 1, 2].map((i) => ( -
- ))} -
- )} -
- Input level -
- - {unavailable && ( - - Previously selected microphone is unavailable — using system default. - - )} -
+ ); } -function CameraSection({ - devices, - saved, - onChange, - stream, - unavailable, -}: { - devices: MediaDeviceInfo[]; - saved: SavedDevice | null; - onChange: (device: SavedDevice | null) => void; - stream: MediaStream | null; - unavailable: boolean; -}) { +function InlineLevelMeter({ stream }: { stream: MediaStream | null }) { + const audioSource = useAudioSource(stream); + if (!audioSource) { + return ( +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ ); + } + return ( +
+
+ +
+
+ ); +} + +function CameraPreview({ stream }: { stream: MediaStream | null }) { const videoRef = useRef(null); - const value = saved?.deviceId ?? SYSTEM_DEFAULT; const hasVideoTrack = (stream?.getVideoTracks().length ?? 0) > 0; useEffect(() => { @@ -191,63 +124,61 @@ function CameraSection({ }, [stream, hasVideoTrack]); return ( -
-
- -

Camera

-
- - -
- {hasVideoTrack ? ( -
- - {unavailable && ( - - Previously selected camera is unavailable — using system default. - +
+ {hasVideoTrack ? ( +
+
+ ); +} + +function DeviceSelect({ + devices, + saved, + onChange, + placeholder, +}: { + devices: MediaDeviceInfo[]; + saved: SavedDevice | null; + onChange: (d: SavedDevice | null) => void; + placeholder: string; +}) { + const value = saved?.deviceId ?? SYSTEM_DEFAULT; + return ( + ); } @@ -285,8 +216,10 @@ export default function AudioVideoSettingsPage() { cameraAvailable, ); - const micUnavailable = !isSavedDeviceAvailable(mic, audioInputs); - const cameraUnavailable = !isSavedDeviceAvailable(camera, videoInputs); + const micUnavailable = + permissionGranted && !isSavedDeviceAvailable(mic, audioInputs); + const cameraUnavailable = + permissionGranted && !isSavedDeviceAvailable(camera, videoInputs); return (
@@ -305,57 +238,72 @@ export default function AudioVideoSettingsPage() {
- {!permissionGranted && ( -
-

- Allow microphone and camera access -

- - Grant access once so llink can show device names and previews. - - - {deviceError && permissionState === "denied" && ( - - {deviceError} +
+ {!permissionGranted && ( +
+
+

Allow device access

+ + Grant permission to see device names and a live preview. + +
+ +
+ )} + + {/* Microphone */} +
+
+ Microphone + +
+ + {micUnavailable && ( + + Saved mic unavailable — using system default. )}
- )} - - - - - - - {previewError && permissionGranted && ( -
- - {previewError} - + {/* Camera */} +
+ Camera + + + {cameraUnavailable && ( + + Saved camera unavailable — using system default. + + )}
- )} + + {(previewError || (deviceError && permissionState === "denied")) && ( + + {previewError ?? deviceError} + + )} +
+ +
+ +
); }