Files
glchat/web/src/lib/useMediaDevices.ts
T
grendervill ecd7b3be10 fix(install): LiveKit перезапускается при изменении конфига
LiveKit читает livekit.yaml только при старте, а compose не замечает правок в
примонтированном файле — из-за этого вебхуки не включались после обновления.
Установщик сравнивает sha256 конфига с сохранённым и перезапускает сервис.
2026-09-20 18:38:54 +03:00

102 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<MediaDevice[]>([]);
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);
},
};
}