diff --git a/src/hooks/useMicrophoneDevices.ts b/src/hooks/useMicrophoneDevices.ts index 17b5374e..4ae21eb2 100644 --- a/src/hooks/useMicrophoneDevices.ts +++ b/src/hooks/useMicrophoneDevices.ts @@ -6,6 +6,8 @@ export interface MicrophoneDevice { groupId: string } +let hasRequestedMicrophoneLabels = false + export function useMicrophoneDevices(enabled: boolean = true) { const [devices, setDevices] = useState([]) const [selectedDeviceId, setSelectedDeviceId] = useState('default') @@ -20,13 +22,14 @@ export function useMicrophoneDevices(enabled: boolean = true) { let mounted = true const loadDevices = async () => { + let permissionStream: MediaStream | null = null + try { setIsLoading(true) setError(null) - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - const allDevices = await navigator.mediaDevices.enumerateDevices() - const audioInputs = allDevices + let allDevices = await navigator.mediaDevices.enumerateDevices() + let audioInputs = allDevices .filter((device) => device.kind === 'audioinput') .map((device) => ({ deviceId: device.deviceId, @@ -34,13 +37,38 @@ export function useMicrophoneDevices(enabled: boolean = true) { groupId: device.groupId, })) - stream.getTracks().forEach((track) => track.stop()) + 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) - if (selectedDeviceId === 'default' && audioInputs.length > 0) { - setSelectedDeviceId(audioInputs[0].deviceId) - } + 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) { @@ -50,6 +78,8 @@ export function useMicrophoneDevices(enabled: boolean = true) { setIsLoading(false) console.error('Error loading microphone devices:', error) } + } finally { + permissionStream?.getTracks().forEach((track) => track.stop()) } } @@ -65,7 +95,7 @@ export function useMicrophoneDevices(enabled: boolean = true) { mounted = false navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange) } - }, [enabled, selectedDeviceId]) + }, [enabled]) return { devices, diff --git a/src/hooks/useVideoDevices.ts b/src/hooks/useVideoDevices.ts index 0571e4f7..228e51f5 100644 --- a/src/hooks/useVideoDevices.ts +++ b/src/hooks/useVideoDevices.ts @@ -6,6 +6,8 @@ export interface VideoDevice { groupId: string } +let hasRequestedVideoLabels = false + export function useVideoDevices(enabled: boolean = true) { const [devices, setDevices] = useState([]) const [selectedDeviceId, setSelectedDeviceId] = useState('default') @@ -20,13 +22,14 @@ export function useVideoDevices(enabled: boolean = true) { let mounted = true const loadDevices = async () => { + let permissionStream: MediaStream | null = null + try { setIsLoading(true) setError(null) - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }) - const allDevices = await navigator.mediaDevices.enumerateDevices() - const videoInputs = allDevices + let allDevices = await navigator.mediaDevices.enumerateDevices() + let videoInputs = allDevices .filter((device) => device.kind === 'videoinput') .map((device, index) => ({ deviceId: device.deviceId, @@ -34,13 +37,38 @@ export function useVideoDevices(enabled: boolean = true) { groupId: device.groupId, })) - stream.getTracks().forEach((track) => track.stop()) + const needsLabelPermission = + videoInputs.length > 0 && videoInputs.every((device) => !device.label.trim()) + + if (needsLabelPermission && !hasRequestedVideoLabels) { + hasRequestedVideoLabels = true + permissionStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }) + allDevices = await navigator.mediaDevices.enumerateDevices() + videoInputs = allDevices + .filter((device) => device.kind === 'videoinput') + .map((device, index) => ({ + deviceId: device.deviceId, + label: device.label || `Camera ${index + 1}`, + groupId: device.groupId, + })) + } if (mounted) { setDevices(videoInputs) - if (selectedDeviceId === 'default' && videoInputs.length > 0) { - setSelectedDeviceId(videoInputs[0].deviceId) - } + setSelectedDeviceId((currentDeviceId) => { + if (currentDeviceId === 'default' && videoInputs.length > 0) { + return videoInputs[0].deviceId + } + + if ( + currentDeviceId !== 'default' && + videoInputs.some((device) => device.deviceId === currentDeviceId) + ) { + return currentDeviceId + } + + return videoInputs[0]?.deviceId ?? 'default' + }) setIsLoading(false) } } catch (error) { @@ -50,6 +78,8 @@ export function useVideoDevices(enabled: boolean = true) { setIsLoading(false) console.error('Error loading video devices:', error) } + } finally { + permissionStream?.getTracks().forEach((track) => track.stop()) } } @@ -65,7 +95,7 @@ export function useVideoDevices(enabled: boolean = true) { mounted = false navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange) } - }, [enabled, selectedDeviceId]) + }, [enabled]) return { devices,