mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-10 07:53:57 +00:00
Enumerate cameras and microphones before requesting media permissions, only opening a stream once when labels need to be unlocked. Stop rerunning enumeration when the selected device changes so launch controls no longer churn hardware access on routine UI interaction.
108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
|
|
export interface MicrophoneDevice {
|
|
deviceId: string
|
|
label: string
|
|
groupId: string
|
|
}
|
|
|
|
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)
|
|
|
|
useEffect(() => {
|
|
if (!enabled) {
|
|
return
|
|
}
|
|
|
|
let mounted = true
|
|
|
|
const loadDevices = async () => {
|
|
let permissionStream: MediaStream | null = 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,
|
|
}))
|
|
|
|
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 (mounted) {
|
|
setDevices(audioInputs)
|
|
setSelectedDeviceId((currentDeviceId) => {
|
|
if (currentDeviceId === 'default' && audioInputs.length > 0) {
|
|
return audioInputs[0].deviceId
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
void loadDevices()
|
|
|
|
const handleDeviceChange = () => {
|
|
void loadDevices()
|
|
}
|
|
|
|
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange)
|
|
|
|
return () => {
|
|
mounted = false
|
|
navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange)
|
|
}
|
|
}, [enabled])
|
|
|
|
return {
|
|
devices,
|
|
selectedDeviceId,
|
|
setSelectedDeviceId,
|
|
isLoading,
|
|
error,
|
|
}
|
|
}
|