Skip to content

Commit efb4997

Browse files
committed
fix: missing audio devices hook
1 parent 7823971 commit efb4997

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { useState, useEffect, useCallback } from "react";
2+
3+
export interface AudioDevice {
4+
deviceId: string;
5+
label: string;
6+
isDefault?: boolean;
7+
}
8+
9+
export function useAudioDevices() {
10+
const [devices, setDevices] = useState<AudioDevice[]>([]);
11+
const [defaultDeviceName, setDefaultDeviceName] = useState<string>("");
12+
13+
const fetchDevices = useCallback(async () => {
14+
if (!navigator.mediaDevices) {
15+
console.warn("Media devices API not available");
16+
return;
17+
}
18+
19+
try {
20+
// Request permissions if needed by getting a stream
21+
// This ensures device labels are available
22+
const stream = await navigator.mediaDevices
23+
.getUserMedia({ audio: true })
24+
.catch(() => null);
25+
26+
if (stream) {
27+
stream.getTracks().forEach((track) => track.stop());
28+
}
29+
30+
// Enumerate devices
31+
const allDevices = await navigator.mediaDevices.enumerateDevices();
32+
33+
// Find the default device name
34+
let foundDefaultName = "";
35+
const defaultDevice = allDevices.find(
36+
(device) =>
37+
device.kind === "audioinput" &&
38+
device.label.toLowerCase().startsWith("default"),
39+
);
40+
41+
if (defaultDevice) {
42+
// Extract the actual device name from "Default - DeviceName" or "Default (DeviceName)"
43+
const match = defaultDevice.label.match(
44+
/Default\s*[-]\s*(.+)|Default\s*\((.+)\)/i,
45+
);
46+
if (match) {
47+
foundDefaultName = match[1] || match[2] || "";
48+
}
49+
}
50+
51+
// Filter and deduplicate audio inputs
52+
const seenDeviceIds = new Set<string>();
53+
const audioInputs = allDevices
54+
.filter((device) => device.kind === "audioinput")
55+
.filter((device) => {
56+
// Skip virtual devices
57+
const lowerLabel = device.label.toLowerCase();
58+
if (
59+
lowerLabel.includes("virtual") ||
60+
lowerLabel.includes("teams") ||
61+
lowerLabel.includes("zoom") ||
62+
lowerLabel.includes("discord")
63+
) {
64+
return false;
65+
}
66+
67+
// Skip "Default" entries entirely - we'll add our own
68+
if (lowerLabel.startsWith("default")) {
69+
return false;
70+
}
71+
72+
// Skip duplicate device IDs
73+
if (seenDeviceIds.has(device.deviceId)) {
74+
return false;
75+
}
76+
seenDeviceIds.add(device.deviceId);
77+
78+
return true;
79+
})
80+
.map((device) => ({
81+
deviceId: device.deviceId,
82+
label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,
83+
}));
84+
85+
// Add system default as first option
86+
const devicesWithDefault: AudioDevice[] = [
87+
{
88+
deviceId: "default",
89+
label: foundDefaultName
90+
? `System Default (${foundDefaultName})`
91+
: "System Default",
92+
isDefault: true,
93+
},
94+
...audioInputs,
95+
];
96+
97+
setDevices(devicesWithDefault);
98+
setDefaultDeviceName(foundDefaultName);
99+
} catch (error) {
100+
console.error("Failed to fetch audio devices:", error);
101+
}
102+
}, []);
103+
104+
useEffect(() => {
105+
fetchDevices();
106+
107+
// Set up device change listener
108+
const handleDeviceChange = () => {
109+
console.log("Audio devices changed");
110+
fetchDevices();
111+
};
112+
113+
navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange);
114+
115+
return () => {
116+
navigator.mediaDevices.removeEventListener(
117+
"devicechange",
118+
handleDeviceChange,
119+
);
120+
};
121+
}, [fetchDevices]);
122+
123+
return { devices, defaultDeviceName, refetch: fetchDevices };
124+
}

0 commit comments

Comments
 (0)