From e717c812a733ea66f257909914ec6adfc32f174e Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:07:34 +1100 Subject: [PATCH] fix invalid/stale mic selection windows --- src/components/launch/LaunchWindow.tsx | 21 +-- src/hooks/useMicrophoneDevices.ts | 178 +++++++++++++------------ 2 files changed, 105 insertions(+), 94 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 09684c7c..f9ba3fe5 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -111,8 +111,8 @@ function DropdownItem({ ); } -function Separator() { - return
; +function Separator({ dropdown = false }: { dropdown?: boolean }) { + return
; } 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() { /> - + - + - + { setMicrophoneEnabled(true); setSelectedDeviceId(device.deviceId); - setMicrophoneDeviceId(device.deviceId); + setMicrophoneDeviceId( + device.deviceId === "default" ? undefined : device.deviceId, + ); }} /> ))} diff --git a/src/hooks/useMicrophoneDevices.ts b/src/hooks/useMicrophoneDevices.ts index 4ae21eb2..f47710cf 100644 --- a/src/hooks/useMicrophoneDevices.ts +++ b/src/hooks/useMicrophoneDevices.ts @@ -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([]) - const [selectedDeviceId, setSelectedDeviceId] = useState('default') - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) +export function useMicrophoneDevices(enabled: boolean = true, preferredDeviceId?: string) { + const [devices, setDevices] = useState([]); + const [selectedDeviceId, setSelectedDeviceId] = useState("default"); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(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, + }; }