From cb156bd99d68f8bae3592683da48d400cbee99b5 Mon Sep 17 00:00:00 2001 From: inmake Date: Fri, 27 Feb 2026 15:21:28 +0500 Subject: [PATCH] Update footer copyright year to 2026 and enhance media device management in SetNameModal and StreamPage components. Added microphone and camera status handling, device selection, and improved user experience with updated state management in the stream store. --- src/App.tsx | 2 +- src/components/modals/stream/SetNameModal.tsx | 333 +++++++++++++++++- src/i18n.ts | 22 ++ src/pages/StreamPage.tsx | 75 +++- src/stores/useStreamStore.ts | 8 + 5 files changed, 422 insertions(+), 18 deletions(-) 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" && ( + + )} +
+