fix invalid/stale mic selection windows

This commit is contained in:
webadderall
2026-03-29 11:07:34 +11:00
parent beb64e88f6
commit e717c812a7
2 changed files with 105 additions and 94 deletions
+13 -8
View File
@@ -111,8 +111,8 @@ function DropdownItem({
);
}
function Separator() {
return <div className={styles.sep} />;
function Separator({ dropdown = false }: { dropdown?: boolean }) {
return <div className={dropdown ? styles.ddSep : styles.sep} />;
}
function MicDeviceRow({
@@ -196,6 +196,7 @@ export function LaunchWindow() {
const showWebcamControls = webcamEnabled && !recording;
const { devices, selectedDeviceId, setSelectedDeviceId } = useMicrophoneDevices(
microphoneEnabled || micDropdownOpen,
microphoneDeviceId,
);
const {
devices: videoDevices,
@@ -206,9 +207,11 @@ export function LaunchWindow() {
const supportsHudCaptureProtection = platform !== "linux";
useEffect(() => {
if (selectedDeviceId && selectedDeviceId !== "default") {
setMicrophoneDeviceId(selectedDeviceId);
if (!selectedDeviceId) {
return;
}
setMicrophoneDeviceId(selectedDeviceId === "default" ? undefined : selectedDeviceId);
}, [selectedDeviceId, setMicrophoneDeviceId]);
useEffect(() => {
@@ -691,7 +694,7 @@ export function LaunchWindow() {
/>
</button>
<Separator />
<Separator />
<IconButton
onClick={toggleMicrophone}
@@ -719,7 +722,7 @@ export function LaunchWindow() {
<Timer size={18} />
</IconButton>
<Separator />
<Separator />
<button
type="button"
@@ -731,7 +734,7 @@ export function LaunchWindow() {
<div className={styles.recDot} />
</button>
<Separator />
<Separator />
<IconButton
buttonRef={moreButtonRef}
@@ -878,7 +881,9 @@ export function LaunchWindow() {
onSelect={() => {
setMicrophoneEnabled(true);
setSelectedDeviceId(device.deviceId);
setMicrophoneDeviceId(device.deviceId);
setMicrophoneDeviceId(
device.deviceId === "default" ? undefined : device.deviceId,
);
}}
/>
))}
+92 -86
View File
@@ -1,107 +1,113 @@
import { useEffect, useState } from 'react'
import { useEffect, useState } from "react";
export interface MicrophoneDevice {
deviceId: string
label: string
groupId: string
deviceId: string;
label: string;
groupId: string;
}
let hasRequestedMicrophoneLabels = false
let hasRequestedMicrophoneLabels = false;
export function useMicrophoneDevices(enabled: boolean = true) {
const [devices, setDevices] = useState<MicrophoneDevice[]>([])
const [selectedDeviceId, setSelectedDeviceId] = useState<string>('default')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
export function useMicrophoneDevices(enabled: boolean = true, preferredDeviceId?: string) {
const [devices, setDevices] = useState<MicrophoneDevice[]>([]);
const [selectedDeviceId, setSelectedDeviceId] = useState<string>("default");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!enabled) {
return
}
useEffect(() => {
if (!enabled) {
return;
}
let mounted = true
let mounted = true;
const loadDevices = async () => {
let permissionStream: MediaStream | null = null
const loadDevices = async () => {
let permissionStream: MediaStream | null = null;
try {
setIsLoading(true)
setError(null)
try {
setIsLoading(true);
setError(null);
let allDevices = await navigator.mediaDevices.enumerateDevices()
let audioInputs = allDevices
.filter((device) => device.kind === 'audioinput')
.map((device) => ({
deviceId: device.deviceId,
label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,
groupId: device.groupId,
}))
let allDevices = await navigator.mediaDevices.enumerateDevices();
let audioInputs = allDevices
.filter((device) => device.kind === "audioinput")
.map((device) => ({
deviceId: device.deviceId,
label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,
groupId: device.groupId,
}));
const needsLabelPermission =
audioInputs.length > 0 && audioInputs.every((device) => !device.label.trim())
const needsLabelPermission =
audioInputs.length > 0 && audioInputs.every((device) => !device.label.trim());
if (needsLabelPermission && !hasRequestedMicrophoneLabels) {
hasRequestedMicrophoneLabels = true
permissionStream = await navigator.mediaDevices.getUserMedia({ audio: true })
allDevices = await navigator.mediaDevices.enumerateDevices()
audioInputs = allDevices
.filter((device) => device.kind === 'audioinput')
.map((device) => ({
deviceId: device.deviceId,
label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,
groupId: device.groupId,
}))
}
if (needsLabelPermission && !hasRequestedMicrophoneLabels) {
hasRequestedMicrophoneLabels = true;
permissionStream = await navigator.mediaDevices.getUserMedia({ audio: true });
allDevices = await navigator.mediaDevices.enumerateDevices();
audioInputs = allDevices
.filter((device) => device.kind === "audioinput")
.map((device) => ({
deviceId: device.deviceId,
label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,
groupId: device.groupId,
}));
}
if (mounted) {
setDevices(audioInputs)
setSelectedDeviceId((currentDeviceId) => {
if (currentDeviceId === 'default' && audioInputs.length > 0) {
return audioInputs[0].deviceId
}
if (mounted) {
setDevices(audioInputs);
setSelectedDeviceId((currentDeviceId) => {
const normalizedPreferredDeviceId = preferredDeviceId ?? "default";
if (audioInputs.some((device) => device.deviceId === normalizedPreferredDeviceId)) {
return normalizedPreferredDeviceId;
}
if (
currentDeviceId !== 'default' &&
audioInputs.some((device) => device.deviceId === currentDeviceId)
) {
return currentDeviceId
}
if (
currentDeviceId !== "default" &&
audioInputs.some((device) => device.deviceId === currentDeviceId)
) {
return currentDeviceId;
}
return audioInputs[0]?.deviceId ?? 'default'
})
setIsLoading(false)
}
} catch (error) {
if (mounted) {
const message = error instanceof Error ? error.message : 'Failed to enumerate audio devices'
setError(message)
setIsLoading(false)
console.error('Error loading microphone devices:', error)
}
} finally {
permissionStream?.getTracks().forEach((track) => track.stop())
}
}
return (
audioInputs.find((device) => device.deviceId !== "default")?.deviceId ??
audioInputs[0]?.deviceId ??
"default"
);
});
setIsLoading(false);
}
} catch (error) {
if (mounted) {
const message =
error instanceof Error ? error.message : "Failed to enumerate audio devices";
setError(message);
setIsLoading(false);
console.error("Error loading microphone devices:", error);
}
} finally {
permissionStream?.getTracks().forEach((track) => track.stop());
}
};
void loadDevices()
void loadDevices();
const handleDeviceChange = () => {
void loadDevices()
}
const handleDeviceChange = () => {
void loadDevices();
};
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange)
navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange);
return () => {
mounted = false
navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange)
}
}, [enabled])
return () => {
mounted = false;
navigator.mediaDevices.removeEventListener("devicechange", handleDeviceChange);
};
}, [enabled, preferredDeviceId]);
return {
devices,
selectedDeviceId,
setSelectedDeviceId,
isLoading,
error,
}
return {
devices,
selectedDeviceId,
setSelectedDeviceId,
isLoading,
error,
};
}