diff --git a/src/App.tsx b/src/App.tsx index ee8d6e3..d4ac8e9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -507,7 +507,7 @@ function App() {

- © 2023 GRAFF interactive.{" "} + © 2026 GRAFF interactive.{" "} Все права защищены.

diff --git a/src/components/modals/stream/SetNameModal.tsx b/src/components/modals/stream/SetNameModal.tsx index ad899db..8539a98 100644 --- a/src/components/modals/stream/SetNameModal.tsx +++ b/src/components/modals/stream/SetNameModal.tsx @@ -1,37 +1,229 @@ -import { ChangeEvent, FormEvent } from "react"; +import { ChangeEvent, FormEvent, useEffect, useRef, useState } from "react"; import Input from "../../ui/Input"; import useStreamStore from "../../../stores/useStreamStore"; import Button from "../../ui/Button"; import useModalStore from "../../../stores/useModalStore"; -import { Trans } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; +import MicroOnIcon from "../../icons/MicroOnIcon"; +import MicroOffIcon from "../../icons/MicroOffIcon"; +import CameraOnIcon from "../../icons/CameraOnIcon"; +import CameraOffIcon from "../../icons/CameraOffIcon"; interface Props { - onAction: () => void; + onAction: ( + audioStream: MediaStream | null, + selectedAudioDeviceId: string, + videoStream: MediaStream | null, + selectedVideoDeviceId: string + ) => void; } function SetNameModal({ onAction }: Props) { + const { t } = useTranslation(); const { name, setName } = useStreamStore(); const { setModal } = useModalStore(); + const [micStatus, setMicStatus] = useState< + "checking" | "success" | "error" + >("checking"); + const [cameraStatus, setCameraStatus] = useState< + "checking" | "success" | "error" + >("checking"); + const [audioDevices, setAudioDevices] = useState([]); + const [videoDevices, setVideoDevices] = useState([]); + const [selectedAudioDeviceId, setSelectedAudioDeviceId] = + useState(""); + const [selectedVideoDeviceId, setSelectedVideoDeviceId] = + useState(""); + const [audioStream, setAudioStream] = useState(null); + const [videoStream, setVideoStream] = useState(null); + const audioStreamRef = useRef(null); + const videoStreamRef = useRef(null); + + async function checkDevices(audioDeviceId?: string, videoDeviceId?: string) { + setMicStatus("checking"); + setCameraStatus("checking"); + audioStreamRef.current?.getTracks().forEach((track) => track.stop()); + videoStreamRef.current?.getTracks().forEach((track) => track.stop()); + audioStreamRef.current = null; + videoStreamRef.current = null; + + try { + const constraints: MediaStreamConstraints = { + audio: audioDeviceId + ? { deviceId: { exact: audioDeviceId } } + : true, + video: videoDeviceId + ? { deviceId: { exact: videoDeviceId } } + : true, + }; + const stream = await navigator.mediaDevices.getUserMedia(constraints); + + const devices = await navigator.mediaDevices.enumerateDevices(); + const microphones = devices.filter((d) => d.kind === "audioinput"); + const cameras = devices.filter((d) => d.kind === "videoinput"); + + const audioTracks = stream.getAudioTracks(); + const videoTracks = stream.getVideoTracks(); + + if (audioTracks.length) { + const audioMediaStream = new MediaStream(audioTracks); + audioStreamRef.current = audioMediaStream; + setAudioStream(audioMediaStream); + setAudioDevices(microphones); + setSelectedAudioDeviceId( + audioDeviceId ?? microphones[0]?.deviceId ?? "" + ); + setMicStatus("success"); + } else { + setMicStatus("error"); + } + + if (videoTracks.length) { + const videoMediaStream = new MediaStream(videoTracks); + videoStreamRef.current = videoMediaStream; + setVideoStream(videoMediaStream); + setVideoDevices(cameras); + setSelectedVideoDeviceId( + videoDeviceId ?? cameras[0]?.deviceId ?? "" + ); + setCameraStatus("success"); + } else { + setCameraStatus("error"); + } + } catch { + setMicStatus("error"); + setCameraStatus("error"); + } + } + + async function checkMicrophone(deviceId?: string) { + setMicStatus("checking"); + audioStreamRef.current?.getTracks().forEach((track) => track.stop()); + audioStreamRef.current = null; + try { + const constraints: MediaStreamConstraints = { + audio: deviceId ? { deviceId: { exact: deviceId } } : true, + }; + const stream = await navigator.mediaDevices.getUserMedia(constraints); + + const devices = await navigator.mediaDevices.enumerateDevices(); + const microphones = devices.filter((d) => d.kind === "audioinput"); + + audioStreamRef.current = new MediaStream(stream.getAudioTracks()); + setAudioStream(audioStreamRef.current); + setAudioDevices(microphones); + setSelectedAudioDeviceId(deviceId ?? microphones[0]?.deviceId ?? ""); + setMicStatus("success"); + } catch { + setMicStatus("error"); + } + } + + async function checkCamera(deviceId?: string) { + setCameraStatus("checking"); + videoStreamRef.current?.getTracks().forEach((track) => track.stop()); + videoStreamRef.current = null; + try { + const constraints: MediaStreamConstraints = { + video: deviceId ? { deviceId: { exact: deviceId } } : true, + }; + const stream = await navigator.mediaDevices.getUserMedia(constraints); + + const devices = await navigator.mediaDevices.enumerateDevices(); + const cameras = devices.filter((d) => d.kind === "videoinput"); + + videoStreamRef.current = new MediaStream(stream.getVideoTracks()); + setVideoStream(videoStreamRef.current); + setVideoDevices(cameras); + setSelectedVideoDeviceId(deviceId ?? cameras[0]?.deviceId ?? ""); + setCameraStatus("success"); + } catch { + setCameraStatus("error"); + } + } + + useEffect(() => { + checkDevices(); + return () => { + audioStreamRef.current?.getTracks().forEach((track) => track.stop()); + videoStreamRef.current?.getTracks().forEach((track) => track.stop()); + }; + }, []); + + function handleAudioDeviceChange(e: ChangeEvent) { + const id = e.target.value; + setSelectedAudioDeviceId(id); + if (videoStreamRef.current) { + checkMicrophone(id || undefined); + } else { + checkDevices(id || undefined, selectedVideoDeviceId || undefined); + } + } + + function handleVideoDeviceChange(e: ChangeEvent) { + const id = e.target.value; + setSelectedVideoDeviceId(id); + if (audioStreamRef.current) { + checkCamera(id || undefined); + } else { + checkDevices(selectedAudioDeviceId || undefined, id || undefined); + } + } + function handleChangeName(e: ChangeEvent) { setName(e.target.value); } + function handleAction( + audio: MediaStream | null, + audioId: string, + video: MediaStream | null, + videoId: string + ) { + audioStreamRef.current = null; + videoStreamRef.current = null; + setModal(null); + onAction(audio, audioId, video, videoId); + } + function handleClickNoName() { setName("Guest"); - setModal(null); - onAction(); + handleAction( + audioStream, + selectedAudioDeviceId, + videoStream, + selectedVideoDeviceId + ); } function handleSubmit(e: FormEvent) { e.preventDefault(); - setModal(null); - onAction(); + handleAction( + audioStream, + selectedAudioDeviceId, + videoStream, + selectedVideoDeviceId + ); + } + + function getDeviceLabel( + device: MediaDeviceInfo, + fallbackKey: string, + prefixKey: string + ) { + let label = + device.label || + (device.deviceId + ? `${t(prefixKey)} ${device.deviceId.slice(0, 8)}` + : t(fallbackKey)); + label = label.replace(/\s*\([0-9a-fA-F]+:[0-9a-fA-F]+\)\s*$/, "").trim(); + return label; } return (
-
+

Здравствуйте!

@@ -57,9 +249,132 @@ function SetNameModal({ onAction }: Props) { onChange={handleChangeName} autoFocus={!name} required - className="max-sm:w-full" + className="!w-full" />
+ +
+

+ Микрофон +

+
+ {micStatus === "checking" && ( +

+ + Проверка микрофона... + +

+ )} + {micStatus === "success" && ( +
+ + + + Микрофон подключен + + +
+ )} + {micStatus === "error" && ( +
+ + + {t("setName.micError")} + + +
+ )} +
+ {audioDevices.length >= 1 && micStatus === "success" && ( + + )} +
+ +
+

+ Камера +

+
+ {cameraStatus === "checking" && ( +

+ + Проверка камеры... + +

+ )} + {cameraStatus === "success" && ( +
+ + + + Камера подключена + + +
+ )} + {cameraStatus === "error" && ( +
+ + + {t("setName.cameraError")} + + +
+ )} +
+ {videoDevices.length >= 1 && cameraStatus === "success" && ( + + )} +
+