Files
Recordly/src/hooks/useVideoDevices.ts
T
webadderall 08745a129b perf: avoid repeated device label capture
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.
2026-03-22 21:02:42 +11:00

108 lines
3.1 KiB
TypeScript

import { useEffect, useState } from 'react'
export interface VideoDevice {
deviceId: string
label: string
groupId: string
}
let hasRequestedVideoLabels = false
export function useVideoDevices(enabled: boolean = true) {
const [devices, setDevices] = useState<VideoDevice[]>([])
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 videoInputs = allDevices
.filter((device) => device.kind === 'videoinput')
.map((device, index) => ({
deviceId: device.deviceId,
label: device.label || `Camera ${index + 1}`,
groupId: device.groupId,
}))
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)
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) {
if (mounted) {
const message = error instanceof Error ? error.message : 'Failed to enumerate video devices'
setError(message)
setIsLoading(false)
console.error('Error loading video 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,
}
}