diff --git a/client/src/components/modals/SettingsModal.tsx b/client/src/components/modals/SettingsModal.tsx index d00a941..367d625 100644 --- a/client/src/components/modals/SettingsModal.tsx +++ b/client/src/components/modals/SettingsModal.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +/* eslint-disable react-hooks/exhaustive-deps */ +import { useState, useRef, useEffect } from "react"; import SoundIcon from "../icons/SoundIcon"; import VideoFilledIcon from "../icons/VideoFilledIcon"; import ModalWrapper from "../ModalWrapper"; @@ -7,38 +8,333 @@ import RangeInput from "../ui/RangeInput"; import Select from "../ui/Select"; import MicrophoneFilledIcon from "../icons/MicrophoneFilledIcon"; import Switch from "../ui/Switch"; +import clsx from "clsx"; +import useModalStore from "../../store/modalStore"; +import SoundCheckModal from "./SoundCheckModal"; +import VoiceCheckModal from "./VoiceCheckModal"; +import LoaderIcon from "../icons/LoaderIcon"; -const microphoneOptions = ["Realtek HD Microphone"]; -const speakerOptions = ["Realtek HD Audio"]; -const cameraOptions = ["Realtek HD Camera"]; +interface MediaDevice { + deviceId: string; + label: string; +} function SettingsModal() { const [microphoneVolume, setMicrophoneVolume] = useState(50); const [speakerVolume, setSpeakerVolume] = useState(50); - const [selectedMicrophone, setSelectedMicrophone] = useState( - microphoneOptions[0] - ); - const [selectedSpeaker, setSelectedSpeaker] = useState(speakerOptions[0]); - const [camera, setCamera] = useState(cameraOptions[0]); + // Списки устройств + const [microphones, setMicrophones] = useState([]); + const [speakers, setSpeakers] = useState([]); + const [cameras, setCameras] = useState([]); + + // Выбранные устройства + const [selectedMicrophone, setSelectedMicrophone] = useState(""); + const [selectedSpeaker, setSelectedSpeaker] = useState(""); + const [selectedCamera, setSelectedCamera] = useState(""); const [mediaType, setMediaType] = useState<"sound" | "video">("sound"); const [participantsVideosHidden, setParticipantsVideosHidden] = useState(false); + const [isVideoTestingError, setIsVideoTestingError] = useState(false); + const [isVideoTestingLoading, setIsVideoTestingLoading] = useState(false); + const [isVideoTesting, setIsVideoTesting] = useState(false); + const videoRef = useRef(null); + const streamRef = useRef(null); + + const [isLoadingMicrophones, setIsLoadingMicrophones] = useState(false); + const [isLoadingSpeakers, setIsLoadingSpeakers] = useState(false); + const [isLoadingCameras, setIsLoadingCameras] = useState(false); + + const { setModal } = useModalStore(); + + // Остановка видео + function stopVideoTest() { + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + } + if (videoRef.current) videoRef.current.srcObject = null; + + setIsVideoTesting(false); + } + + // Загрузка аудио устройств + async function loadAudioDevices() { + setIsLoadingMicrophones(true); + setIsLoadingSpeakers(true); + + try { + // Запрашиваем разрешения на аудио + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + + // Сразу останавливаем стрим после получения разрешений + stream.getTracks().forEach((track) => track.stop()); + + const devices = await navigator.mediaDevices.enumerateDevices(); + console.log("Найдено аудио устройств:", devices); + + const audioInputs: MediaDevice[] = []; + const audioOutputs: MediaDevice[] = []; + + devices.forEach((device) => { + const deviceInfo: MediaDevice = { + deviceId: device.deviceId, + label: + device.label || `${device.kind} (${device.deviceId.slice(0, 8)})`, + }; + + if (device.kind === "audioinput") { + audioInputs.push(deviceInfo); + } else if (device.kind === "audiooutput") { + audioOutputs.push(deviceInfo); + } + }); + + console.log("Микрофоны:", audioInputs); + console.log("Динамики:", audioOutputs); + + setMicrophones(audioInputs); + setIsLoadingMicrophones(false); + + setSpeakers(audioOutputs); + setIsLoadingSpeakers(false); + + // Устанавливаем первое устройство по умолчанию + if (audioInputs.length > 0 && !selectedMicrophone) { + setSelectedMicrophone(audioInputs[0].label); + } + if (audioOutputs.length > 0 && !selectedSpeaker) { + setSelectedSpeaker(audioOutputs[0].label); + } + } catch (error) { + console.error("Ошибка при загрузке аудио устройств:", error); + + // Пробуем загрузить устройства без разрешений + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + console.log("Аудио устройства без разрешений:", devices); + + const audioInputs: MediaDevice[] = []; + const audioOutputs: MediaDevice[] = []; + + devices.forEach((device) => { + const deviceInfo: MediaDevice = { + deviceId: device.deviceId, + label: device.label || `${device.kind} (требуется разрешение)`, + }; + + if (device.kind === "audioinput") { + audioInputs.push(deviceInfo); + } else if (device.kind === "audiooutput") { + audioOutputs.push(deviceInfo); + } + }); + + setMicrophones(audioInputs); + setSpeakers(audioOutputs); + } catch (err) { + console.error("Не удалось загрузить аудио устройства:", err); + } finally { + setIsLoadingMicrophones(false); + setIsLoadingSpeakers(false); + } + } + } + + // Загрузка видео устройств + async function loadVideoDevices() { + setIsLoadingCameras(true); + + try { + // Запрашиваем разрешения на видео + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + }); + + // Сразу останавливаем стрим после получения разрешений + stream.getTracks().forEach((track) => track.stop()); + + const devices = await navigator.mediaDevices.enumerateDevices(); + console.log("Найдено видео устройств:", devices); + + const videoInputs: MediaDevice[] = []; + + devices.forEach((device) => { + if (device.kind === "videoinput") { + const deviceInfo: MediaDevice = { + deviceId: device.deviceId, + label: + device.label || `${device.kind} (${device.deviceId.slice(0, 8)})`, + }; + videoInputs.push(deviceInfo); + } + }); + + console.log("Камеры:", videoInputs); + + setCameras(videoInputs); + setIsLoadingCameras(false); + + // Устанавливаем первое устройство по умолчанию + if (videoInputs.length > 0 && !selectedCamera) { + setSelectedCamera(videoInputs[0].label); + } + } catch (error) { + console.error("Ошибка при загрузке видео устройств:", error); + + // Пробуем загрузить устройства без разрешений + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + console.log("Видео устройства без разрешений:", devices); + + const videoInputs: MediaDevice[] = []; + + devices.forEach((device) => { + if (device.kind === "videoinput") { + const deviceInfo: MediaDevice = { + deviceId: device.deviceId, + label: device.label || `${device.kind} (требуется разрешение)`, + }; + videoInputs.push(deviceInfo); + } + }); + + setCameras(videoInputs); + } catch (err) { + console.error("Не удалось загрузить видео устройства:", err); + } finally { + setIsLoadingCameras(false); + } + } + } + + // Запуск видео + async function startVideoTest() { + try { + setIsVideoTestingLoading(true); + setIsVideoTestingError(false); + + // Находим deviceId выбранной камеры + const selectedCameraDevice = cameras.find( + (cam) => cam.label === selectedCamera + ); + + const constraints: MediaStreamConstraints = { + video: selectedCameraDevice + ? { deviceId: { exact: selectedCameraDevice.deviceId } } + : true, + audio: false, + }; + + const stream = await navigator.mediaDevices.getUserMedia(constraints); + streamRef.current = stream; + if (videoRef.current) videoRef.current.srcObject = stream; + + setIsVideoTesting(true); + } catch (error) { + console.error("Ошибка при доступе к камере:", error); + setIsVideoTestingError(true); + } finally { + setIsVideoTestingLoading(false); + } + } + + // Загружаем только аудио устройства при монтировании + useEffect(() => { + loadAudioDevices(); + + // Слушаем изменения устройств (подключение/отключение) + const handleDeviceChange = () => { + // Загружаем аудио всегда + loadAudioDevices(); + // Загружаем видео только если находимся на вкладке "Видео" + if (mediaType === "video") { + loadVideoDevices(); + } + }; + + navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange); + + return () => { + navigator.mediaDevices.removeEventListener( + "devicechange", + handleDeviceChange + ); + }; + }, [mediaType]); + + // Загружаем видео устройства и запускаем видео при переключении на вкладку "Видео" + useEffect(() => { + if (mediaType === "video") { + // Всегда загружаем камеры при переходе на вкладку, если их еще нет + if (cameras.length === 0 && !isLoadingCameras) { + loadVideoDevices(); + } + + // Запускаем видео только если камеры уже загружены + if (cameras.length > 0) { + startVideoTest(); + } + + return stopVideoTest; + } + }, [mediaType, cameras.length]); + + // Перезапускаем видео при смене камеры + useEffect(() => { + if (isVideoTesting && selectedCamera) { + stopVideoTest(); + startVideoTest(); + } + }, [selectedCamera]); + + // Открыть модальное окно проверки микрофона + const openMicrophoneCheck = () => { + setModal( + setModal(null)} + /> + ); + }; + + // Открыть модальное окно проверки динамика + const openSpeakerCheck = () => { + setModal( + setModal(null)} + /> + ); + }; + return ( - +
+
+ )}
@@ -92,17 +425,48 @@ function SettingsModal() {

Динамик

-
- s.label)} + defaultOption={selectedSpeaker} + onSelect={setSelectedSpeaker} + /> + +
+ )}
@@ -123,24 +487,71 @@ function SettingsModal() {

Камера

- c.label)} + onSelect={setSelectedCamera} + defaultOption={selectedCamera} + /> + )} +