Files
stream.graff.tech-new/client/src/components/modals/SettingsModal.tsx
T

553 lines
21 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.
/* 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";
import Button from "../ui/Button";
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";
interface MediaDevice {
deviceId: string;
label: string;
}
function SettingsModal() {
const [microphoneVolume, setMicrophoneVolume] = useState(50);
const [speakerVolume, setSpeakerVolume] = useState(50);
// Списки устройств
const [microphones, setMicrophones] = useState<MediaDevice[]>([]);
const [speakers, setSpeakers] = useState<MediaDevice[]>([]);
const [cameras, setCameras] = useState<MediaDevice[]>([]);
// Выбранные устройства
const [selectedMicrophone, setSelectedMicrophone] = useState<string>("");
const [selectedSpeaker, setSelectedSpeaker] = useState<string>("");
const [selectedCamera, setSelectedCamera] = useState<string>("");
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<HTMLVideoElement>(null);
const streamRef = useRef<MediaStream | null>(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})`,
};
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(
<VoiceCheckModal
selectedMicrophone={selectedMicrophone}
microphones={microphones}
microphoneVolume={microphoneVolume}
onSelectMicrophone={setSelectedMicrophone}
/>
);
};
// Открыть модальное окно проверки динамика
const openSpeakerCheck = () => {
setModal(
<SoundCheckModal
selectedSpeaker={selectedSpeaker}
speakers={speakers}
onSelectSpeaker={setSelectedSpeaker}
speakerVolume={speakerVolume}
/>
);
};
return (
<ModalWrapper
title="Настройки"
className="2xl:max-w-[27.778vw] max-w-[400px]"
>
<div className="2xl:space-y-[1.389vw] space-y-5 2xl:-mt-[1.389vw] -mt-5">
<div className="flex">
<Button
variant="menu"
size="large"
className={"w-full"}
onClick={() => setMediaType("sound")}
isActive={mediaType === "sound"}
>
<div
className={clsx(
"2xl:size-[1.111vw] size-4",
mediaType === "sound" ? "text-[#7B60F3]" : "text-[#7D7D7D]"
)}
>
<SoundIcon />
</div>
<p className="font-medium">Звук</p>
</Button>
<Button
variant="menu"
size="large"
className="w-full"
onClick={() => setMediaType("video")}
isActive={mediaType === "video"}
>
<div
className={clsx(
"2xl:size-[1.111vw] size-4",
mediaType === "video" ? "text-[#7B60F3]" : "text-[#7D7D7D]"
)}
>
<VideoFilledIcon />
</div>
<p className="font-medium">Видео</p>
</Button>
</div>
{mediaType === "sound" && (
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.833vw] space-y-3">
<p className="title-s font-medium">Микрофон</p>
<div className="2xl:space-y-[1.111vw] space-y-4">
{isLoadingMicrophones ? (
<div className="bg-[#F3F3F3] 2xl:p-[1.667vw] p-6 2xl:rounded-[1.111vw] rounded-2xl flex flex-col items-center 2xl:gap-[0.833vw] gap-3">
<div className="2xl:size-[2.222vw] size-8 text-[#7B60F3] animate-spin">
<LoaderIcon />
</div>
<p className="text-s text-[#7D7D7D]">
Загрузка микрофонов...
</p>
</div>
) : microphones.length === 0 ? (
<div className="bg-[#FEF3F2] 2xl:p-[1.111vw] p-4 2xl:rounded-[1.111vw] rounded-2xl 2xl:space-y-[0.556vw] space-y-2">
<p className="text-s text-[#FF4517]">
Микрофоны не найдены. Проверьте подключение устройства и
разрешения браузера.
</p>
<Button
variant="secondary"
size="medium"
onClick={loadAudioDevices}
className="w-full"
>
Обновить устройства
</Button>
</div>
) : (
<div className="flex 2xl:gap-[0.556vw] gap-2">
<Select
className="flex-1"
options={microphones.map((m) => m.label)}
defaultOption={selectedMicrophone}
onSelect={setSelectedMicrophone}
/>
<Button
variant="cta"
size="large"
onClick={openMicrophoneCheck}
disabled={!selectedMicrophone}
>
Проверить
</Button>
</div>
)}
<div className="flex items-center 2xl:gap-[0.833vw] gap-3">
<div className="2xl:size-[1.111vw] size-4 text-[#7D7D7D]">
<MicrophoneFilledIcon />
</div>
<RangeInput
value={microphoneVolume}
onChange={setMicrophoneVolume}
/>
<p className="caption-xs font-medium text-[#7D7D7D] 2xl:w-[1.667vw] w-6">
{microphoneVolume.toFixed(0)}%
</p>
</div>
</div>
</div>
<hr className="bg-[#F6F6F6] 2xl:h-[0.069vw] h-px" />
<div className="2xl:space-y-[0.833vw] space-y-3">
<p className="title-s font-medium">Динамик</p>
<div className="2xl:space-y-[1.111vw] space-y-4">
{isLoadingSpeakers ? (
<div className="bg-[#F3F3F3] 2xl:p-[1.667vw] p-6 2xl:rounded-[1.111vw] rounded-2xl flex flex-col items-center 2xl:gap-[0.833vw] gap-3">
<div className="2xl:size-[2.222vw] size-8 text-[#7B60F3] animate-spin">
<LoaderIcon />
</div>
<p className="text-s text-[#7D7D7D]">
Загрузка динамиков...
</p>
</div>
) : speakers.length === 0 ? (
<div className="bg-[#FEF3F2] 2xl:p-[1.111vw] p-4 2xl:rounded-[1.111vw] rounded-2xl 2xl:space-y-[0.556vw] space-y-2">
<p className="text-s text-[#FF4517]">
Динамики не найдены. Проверьте подключение устройства и
разрешения браузера.
</p>
<Button
variant="secondary"
size="medium"
onClick={loadAudioDevices}
className="w-full"
>
Обновить устройства
</Button>
</div>
) : (
<div className="flex 2xl:gap-[0.556vw] gap-2">
<Select
className="flex-1"
options={speakers.map((s) => s.label)}
defaultOption={selectedSpeaker}
onSelect={setSelectedSpeaker}
/>
<Button
variant="cta"
size="large"
onClick={openSpeakerCheck}
disabled={!selectedSpeaker}
>
Проверить
</Button>
</div>
)}
<div className="flex items-center 2xl:gap-[0.833vw] gap-3">
<div className="2xl:size-[1.111vw] size-4 text-[#7D7D7D]">
<SoundIcon />
</div>
<RangeInput
value={speakerVolume}
onChange={setSpeakerVolume}
/>
<p className="caption-xs font-medium text-[#7D7D7D] 2xl:w-[1.667vw] w-6">
{speakerVolume.toFixed(0)}%
</p>
</div>
</div>
</div>
</div>
)}
{mediaType === "video" && (
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.556vw] space-y-2">
<p className="title-s font-medium">Камера</p>
{isLoadingCameras ? (
<div className="bg-[#F3F3F3] 2xl:p-[1.667vw] p-6 2xl:rounded-[1.111vw] rounded-2xl flex flex-col items-center 2xl:gap-[0.833vw] gap-3">
<div className="2xl:size-[2.222vw] size-8 text-[#7B60F3] animate-spin">
<LoaderIcon />
</div>
<p className="text-s text-[#7D7D7D]">Загрузка камер...</p>
</div>
) : cameras.length > 0 ? (
<Select
options={cameras.map((c) => c.label)}
onSelect={setSelectedCamera}
defaultOption={selectedCamera}
/>
) : null}
<video
ref={videoRef}
muted
autoPlay
playsInline
hidden={!isVideoTesting}
className="2xl:rounded-[1.111vw] rounded-2xl 2xl:w-[25vw] w-[360px] aspect-[360/202] object-cover"
/>
{!isVideoTesting && (
<div className="bg-[#F3F3F3] 2xl:w-[25vw] w-[360px] aspect-[360/202] flex justify-center items-center 2xl:rounded-[1.111vw] rounded-2xl">
{cameras.length === 0 && !isLoadingCameras ? (
<div className="2xl:space-y-[0.556vw] space-y-2 text-center 2xl:px-[1.111vw] px-4">
<p className="title-s font-medium">Камеры не найдены</p>
<p className="text-[#7D7D7D] caption-s font-medium">
Проверьте подключение устройства и разрешения браузера
</p>
</div>
) : isVideoTestingError ? (
<div className="2xl:space-y-[0.556vw] space-y-2">
<p className="title-s font-medium text-center">
Включить видео не удалось
</p>
<p className="text-center text-[#7D7D7D] caption-s font-medium">
Проверьте подключение камеры и разрешения
</p>
</div>
) : isVideoTestingLoading ? (
<div className="2xl:space-y-[0.556vw] space-y-2">
<p className="title-s font-medium text-center">
Проверка видео...
</p>
</div>
) : null}
</div>
)}
</div>
<div className="flex items-center 2xl:gap-[0.556vw] gap-2">
<Switch
enabled={participantsVideosHidden}
onChange={setParticipantsVideosHidden}
/>
<p className="text-m">Скрыть видео участников</p>
</div>
</div>
)}
</div>
</ModalWrapper>
);
}
export default SettingsModal;