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 ? (
-
- ) : (
-
-
-
- {devices.length === 0 ? "No camera detected" : "Preview unavailable"}
-
-
- )}
-
-
- {unavailable && (
-
- Previously selected camera is unavailable — using system default.
-
+
+ {hasVideoTrack ? (
+
+ ) : (
+
+
+ No preview
+
)}
-
+
+ );
+}
+
+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}
+
+ )}
+
+
+
+
+
);
}