import { useEffect, useState } from 'react'; import { useVoiceStore, type VoiceDeviceKind } from '@/stores/voice'; import { useVoiceConnectionStore } from '@/stores/voiceConnection'; /** Устройство ввода/вывода в том виде, в каком его показывает интерфейс. */ export interface MediaDevice { deviceId: string; /** Подпись может быть пустой: браузер скрывает её без разрешения. */ label: string; } export interface MediaDevicesResult { devices: MediaDevice[]; /** Доступен ли `navigator.mediaDevices.enumerateDevices()`. */ supported: boolean; } /** * Список устройств нужного вида через `navigator.mediaDevices`. * * Без разрешения на доступ браузер отдаёт устройства с пустыми подписями — * их показываем как «Устройство N». Если API недоступно (старый браузер или * тестовое окружение), возвращаем `supported: false`. */ export function useMediaDevices(kind: VoiceDeviceKind): MediaDevicesResult { const supported = typeof navigator !== 'undefined' && typeof navigator.mediaDevices?.enumerateDevices === 'function'; const [devices, setDevices] = useState([]); useEffect(() => { if (!supported) { // API нет — список остаётся пустым, интерфейс покажет «недоступны». return; } // Ссылку запоминаем: к моменту очистки эффекта API может быть уже недоступно. const mediaDevices = navigator.mediaDevices; let cancelled = false; const load = (): void => { void mediaDevices .enumerateDevices() .then((list) => { if (cancelled) { return; } setDevices( list .filter((device) => device.kind === kind) .map((device) => ({ deviceId: device.deviceId, label: device.label })), ); }) .catch(() => { if (!cancelled) { setDevices([]); } }); }; load(); // Список меняется при подключении гарнитуры — обновляем его на лету. mediaDevices.addEventListener?.('devicechange', load); return () => { cancelled = true; mediaDevices.removeEventListener?.('devicechange', load); }; }, [kind, supported]); return { devices, supported }; } /** * Выбранное устройство нужного вида из стора предпочтений. * Одна обёртка на три вида — чтобы не дублировать селекторы в компонентах. * * Смена устройства сразу применяется к активной голосовой сессии * (`switchActiveDevice`), а выбор сохраняется в предпочтениях клиента. */ export function useDeviceChoice(kind: VoiceDeviceKind): { value: string; select: (deviceId: string) => void; } { const value = useVoiceStore((state) => { if (kind === 'audioinput') { return state.micDeviceId; } if (kind === 'videoinput') { return state.cameraDeviceId; } return state.outputDeviceId; }); const selectDevice = useVoiceStore((state) => state.selectDevice); return { value, select: (deviceId: string) => { selectDevice(kind, deviceId); void useVoiceConnectionStore.getState().switchActiveDevice(kind, deviceId); }, }; }