Compare commits
2 Commits
42082faf0d
...
c2fc1624a4
| Author | SHA1 | Date | |
|---|---|---|---|
| c2fc1624a4 | |||
| cb156bd99d |
+1
-1
@@ -507,7 +507,7 @@ function App() {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-[#C5C7CE]">
|
<p className="text-xs text-[#C5C7CE]">
|
||||||
© 2023 GRAFF interactive.{" "}
|
© 2026 GRAFF interactive.{" "}
|
||||||
<Trans i18nKey={"footer.text"}>Все права защищены.</Trans>
|
<Trans i18nKey={"footer.text"}>Все права защищены.</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+68
-25
@@ -13,51 +13,94 @@ interface Props {
|
|||||||
user?: IUser;
|
user?: IUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SPEAKING_HIDE_DELAY_MS = 400;
|
||||||
|
|
||||||
function Video({ mediaStream, muted, user }: Props) {
|
function Video({ mediaStream, muted, user }: Props) {
|
||||||
const remoteVideoRef = useRef<HTMLVideoElement>(null);
|
const remoteVideoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const remoteAudioRef = useRef<HTMLAudioElement>(null);
|
||||||
const isSpeaking = useIsAudioActive({ source: mediaStream });
|
const isSpeaking = useIsAudioActive({ source: mediaStream });
|
||||||
|
const [showSpeakingBorder, setShowSpeakingBorder] = useState(false);
|
||||||
|
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const [_muted, setMuted] = useState(muted);
|
const [_muted, setMuted] = useState(muted);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [minimized, setMinimized] = useState(user?.isAdmin ? false : true);
|
const [minimized, setMinimized] = useState(user?.isAdmin ? false : true);
|
||||||
|
|
||||||
|
const hasVideo = (mediaStream?.getVideoTracks().length ?? 0) > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && user.micEnabled === false) {
|
||||||
|
if (hideTimeoutRef.current) {
|
||||||
|
clearTimeout(hideTimeoutRef.current);
|
||||||
|
hideTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
setShowSpeakingBorder(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSpeaking) {
|
||||||
|
if (hideTimeoutRef.current) {
|
||||||
|
clearTimeout(hideTimeoutRef.current);
|
||||||
|
hideTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
setShowSpeakingBorder(true);
|
||||||
|
} else {
|
||||||
|
hideTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowSpeakingBorder(false);
|
||||||
|
hideTimeoutRef.current = null;
|
||||||
|
}, SPEAKING_HIDE_DELAY_MS);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
|
||||||
|
};
|
||||||
|
}, [isSpeaking, user?.micEnabled]);
|
||||||
|
|
||||||
function toggleSound() {
|
function toggleSound() {
|
||||||
if (!remoteVideoRef.current) return;
|
|
||||||
// remoteVideoRef.current.muted = !remoteVideoRef.current.muted;
|
|
||||||
setMuted((prev) => !prev);
|
setMuted((prev) => !prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!remoteVideoRef.current) return;
|
if (!mediaStream) return;
|
||||||
|
|
||||||
remoteVideoRef.current.srcObject = mediaStream;
|
if (hasVideo && remoteVideoRef.current) {
|
||||||
remoteVideoRef.current.onloadedmetadata = () => {
|
remoteVideoRef.current.srcObject = mediaStream;
|
||||||
remoteVideoRef.current?.play();
|
remoteVideoRef.current.onloadedmetadata = () => {
|
||||||
};
|
remoteVideoRef.current?.play();
|
||||||
|
};
|
||||||
remoteVideoRef.current.onplay = () => {
|
remoteVideoRef.current.onplay = () => setIsLoading(false);
|
||||||
|
} else if (!hasVideo && remoteAudioRef.current) {
|
||||||
|
remoteAudioRef.current.srcObject = mediaStream;
|
||||||
|
remoteAudioRef.current.onloadedmetadata = () => {
|
||||||
|
remoteAudioRef.current?.play();
|
||||||
|
};
|
||||||
|
remoteAudioRef.current.onplay = () => setIsLoading(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
}
|
||||||
|
}, [mediaStream, hasVideo]);
|
||||||
console.log("mediaStream", mediaStream?.getTracks());
|
|
||||||
}, [mediaStream]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("remoteVideoRef.current", remoteVideoRef);
|
|
||||||
}, [remoteVideoRef.current]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative border-2 rounded-lg ${
|
className={`relative border-2 rounded-lg ${
|
||||||
minimized ? "h-8 rounded-lg overflow-hidden" : ""
|
minimized ? "h-8 rounded-lg overflow-hidden" : ""
|
||||||
} ${!_muted && user?.micEnabled && isSpeaking ? "border-green-500" : "border-transparent"}`}
|
} ${!_muted && user?.micEnabled !== false && showSpeakingBorder ? "border-green-500" : "border-transparent"}`}
|
||||||
>
|
>
|
||||||
<video
|
{hasVideo ? (
|
||||||
ref={remoteVideoRef}
|
<video
|
||||||
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500`}
|
ref={remoteVideoRef}
|
||||||
playsInline
|
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500`}
|
||||||
autoPlay
|
playsInline
|
||||||
muted={_muted}
|
autoPlay
|
||||||
></video>
|
muted={_muted}
|
||||||
|
></video>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<audio
|
||||||
|
ref={remoteAudioRef}
|
||||||
|
autoPlay
|
||||||
|
muted={_muted}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div className="aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg bg-gray-500" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={`absolute -bottom-1.5 flex items-center justify-between w-full gap-2 p-2 ${
|
className={`absolute -bottom-1.5 flex items-center justify-between w-full gap-2 p-2 ${
|
||||||
minimized ? "bg-black" : ""
|
minimized ? "bg-black" : ""
|
||||||
|
|||||||
@@ -1,37 +1,237 @@
|
|||||||
import { ChangeEvent, FormEvent } from "react";
|
import { ChangeEvent, FormEvent, useEffect, useRef, useState } from "react";
|
||||||
import Input from "../../ui/Input";
|
import Input from "../../ui/Input";
|
||||||
import useStreamStore from "../../../stores/useStreamStore";
|
import useStreamStore from "../../../stores/useStreamStore";
|
||||||
import Button from "../../ui/Button";
|
import Button from "../../ui/Button";
|
||||||
import useModalStore from "../../../stores/useModalStore";
|
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 {
|
interface Props {
|
||||||
onAction: () => void;
|
onAction: (
|
||||||
|
audioStream: MediaStream | null,
|
||||||
|
selectedAudioDeviceId: string,
|
||||||
|
videoStream: MediaStream | null,
|
||||||
|
selectedVideoDeviceId: string
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SetNameModal({ onAction }: Props) {
|
function SetNameModal({ onAction }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { name, setName } = useStreamStore();
|
const { name, setName } = useStreamStore();
|
||||||
const { setModal } = useModalStore();
|
const { setModal } = useModalStore();
|
||||||
|
|
||||||
|
const [micStatus, setMicStatus] = useState<
|
||||||
|
"checking" | "success" | "error"
|
||||||
|
>("checking");
|
||||||
|
const [cameraStatus, setCameraStatus] = useState<
|
||||||
|
"checking" | "success" | "error"
|
||||||
|
>("checking");
|
||||||
|
const [audioDevices, setAudioDevices] = useState<MediaDeviceInfo[]>([]);
|
||||||
|
const [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]);
|
||||||
|
const [selectedAudioDeviceId, setSelectedAudioDeviceId] =
|
||||||
|
useState<string>("");
|
||||||
|
const [selectedVideoDeviceId, setSelectedVideoDeviceId] =
|
||||||
|
useState<string>("");
|
||||||
|
const [audioStream, setAudioStream] = useState<MediaStream | null>(null);
|
||||||
|
const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
|
||||||
|
const audioStreamRef = useRef<MediaStream | null>(null);
|
||||||
|
const videoStreamRef = useRef<MediaStream | null>(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 audioConstraints: MediaStreamConstraints = {
|
||||||
|
audio: audioDeviceId
|
||||||
|
? { deviceId: { exact: audioDeviceId } }
|
||||||
|
: true,
|
||||||
|
};
|
||||||
|
const audioStreamResult = await navigator.mediaDevices.getUserMedia(
|
||||||
|
audioConstraints
|
||||||
|
);
|
||||||
|
const audioTracks = audioStreamResult.getAudioTracks();
|
||||||
|
if (audioTracks.length) {
|
||||||
|
const audioMediaStream = new MediaStream(audioTracks);
|
||||||
|
audioStreamRef.current = audioMediaStream;
|
||||||
|
setAudioStream(audioMediaStream);
|
||||||
|
setMicStatus("success");
|
||||||
|
} else {
|
||||||
|
setMicStatus("error");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setMicStatus("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const videoConstraints: MediaStreamConstraints = {
|
||||||
|
video: videoDeviceId
|
||||||
|
? { deviceId: { exact: videoDeviceId } }
|
||||||
|
: true,
|
||||||
|
};
|
||||||
|
const videoStreamResult = await navigator.mediaDevices.getUserMedia(
|
||||||
|
videoConstraints
|
||||||
|
);
|
||||||
|
const videoTracks = videoStreamResult.getVideoTracks();
|
||||||
|
if (videoTracks.length) {
|
||||||
|
const videoMediaStream = new MediaStream(videoTracks);
|
||||||
|
videoStreamRef.current = videoMediaStream;
|
||||||
|
setVideoStream(videoMediaStream);
|
||||||
|
setCameraStatus("success");
|
||||||
|
} else {
|
||||||
|
setCameraStatus("error");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setCameraStatus("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const microphones = devices.filter((d) => d.kind === "audioinput");
|
||||||
|
const cameras = devices.filter((d) => d.kind === "videoinput");
|
||||||
|
setAudioDevices(microphones);
|
||||||
|
setVideoDevices(cameras);
|
||||||
|
setSelectedAudioDeviceId(
|
||||||
|
audioDeviceId ?? microphones[0]?.deviceId ?? ""
|
||||||
|
);
|
||||||
|
setSelectedVideoDeviceId(
|
||||||
|
videoDeviceId ?? cameras[0]?.deviceId ?? ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<HTMLSelectElement>) {
|
||||||
|
const id = e.target.value;
|
||||||
|
setSelectedAudioDeviceId(id);
|
||||||
|
if (videoStreamRef.current) {
|
||||||
|
checkMicrophone(id || undefined);
|
||||||
|
} else {
|
||||||
|
checkDevices(id || undefined, selectedVideoDeviceId || undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVideoDeviceChange(e: ChangeEvent<HTMLSelectElement>) {
|
||||||
|
const id = e.target.value;
|
||||||
|
setSelectedVideoDeviceId(id);
|
||||||
|
if (audioStreamRef.current) {
|
||||||
|
checkCamera(id || undefined);
|
||||||
|
} else {
|
||||||
|
checkDevices(selectedAudioDeviceId || undefined, id || undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleChangeName(e: ChangeEvent<HTMLInputElement>) {
|
function handleChangeName(e: ChangeEvent<HTMLInputElement>) {
|
||||||
setName(e.target.value);
|
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() {
|
function handleClickNoName() {
|
||||||
setName("Guest");
|
setName("Guest");
|
||||||
setModal(null);
|
handleAction(
|
||||||
onAction();
|
audioStream,
|
||||||
|
selectedAudioDeviceId,
|
||||||
|
videoStream,
|
||||||
|
selectedVideoDeviceId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setModal(null);
|
handleAction(
|
||||||
onAction();
|
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 (
|
return (
|
||||||
<div className="flex items-center justify-center w-full h-full bg-opacity-50 backdrop-blur-2xl">
|
<div className="flex items-center justify-center w-full h-full bg-opacity-50 backdrop-blur-2xl">
|
||||||
<div className="bg-white sm:p-12 p-6 sm:rounded-lg space-y-6 sm:h-auto h-full sm:w-auto w-full flex flex-col max-sm:justify-center">
|
<div className="bg-white sm:p-12 p-6 sm:rounded-lg space-y-6 sm:h-auto h-full sm:w-[494px] w-full flex flex-col max-sm:justify-center max-h-dvh overflow-y-auto shadow">
|
||||||
<p className="text-2xl font-semibold">
|
<p className="text-2xl font-semibold">
|
||||||
<Trans i18nKey={"setName.hello"}>Здравствуйте!</Trans>
|
<Trans i18nKey={"setName.hello"}>Здравствуйте!</Trans>
|
||||||
</p>
|
</p>
|
||||||
@@ -57,9 +257,132 @@ function SetNameModal({ onAction }: Props) {
|
|||||||
onChange={handleChangeName}
|
onChange={handleChangeName}
|
||||||
autoFocus={!name}
|
autoFocus={!name}
|
||||||
required
|
required
|
||||||
className="max-sm:w-full"
|
className="!w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-[#77828C]">
|
||||||
|
<Trans i18nKey={"setName.micDevice"}>Микрофон</Trans>
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
|
{micStatus === "checking" && (
|
||||||
|
<p className="text-sm text-[#77828C]">
|
||||||
|
<Trans i18nKey={"setName.micChecking"}>
|
||||||
|
Проверка микрофона...
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{micStatus === "success" && (
|
||||||
|
<div className="flex gap-2 items-center text-green-600">
|
||||||
|
<MicroOnIcon />
|
||||||
|
<span className="text-sm">
|
||||||
|
<Trans i18nKey={"setName.micSuccess"}>
|
||||||
|
Микрофон подключен
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{micStatus === "error" && (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<MicroOffIcon />
|
||||||
|
<span className="text-sm text-[#EB5757]">
|
||||||
|
{t("setName.micError")}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
videoStreamRef.current
|
||||||
|
? checkMicrophone()
|
||||||
|
: checkDevices()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("setName.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{audioDevices.length >= 1 && micStatus === "success" && (
|
||||||
|
<select
|
||||||
|
value={selectedAudioDeviceId}
|
||||||
|
onChange={handleAudioDeviceChange}
|
||||||
|
className="bg-white border border-[#DAE0E5] w-full h-10 px-2 py-2.5 rounded-lg text-sm outline-none max-sm:w-full"
|
||||||
|
>
|
||||||
|
{audioDevices.map((device) => (
|
||||||
|
<option key={device.deviceId} value={device.deviceId}>
|
||||||
|
{getDeviceLabel(
|
||||||
|
device,
|
||||||
|
"setName.defaultMic",
|
||||||
|
"setName.micDevice"
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-[#77828C]">
|
||||||
|
<Trans i18nKey={"setName.cameraDevice"}>Камера</Trans>
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
|
{cameraStatus === "checking" && (
|
||||||
|
<p className="text-sm text-[#77828C]">
|
||||||
|
<Trans i18nKey={"setName.cameraChecking"}>
|
||||||
|
Проверка камеры...
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{cameraStatus === "success" && (
|
||||||
|
<div className="flex gap-2 items-center text-green-600">
|
||||||
|
<CameraOnIcon />
|
||||||
|
<span className="text-sm">
|
||||||
|
<Trans i18nKey={"setName.cameraSuccess"}>
|
||||||
|
Камера подключена
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cameraStatus === "error" && (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<CameraOffIcon />
|
||||||
|
<span className="text-sm text-[#EB5757]">
|
||||||
|
{t("setName.cameraError")}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
audioStreamRef.current
|
||||||
|
? checkCamera()
|
||||||
|
: checkDevices()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("setName.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{videoDevices.length >= 1 && cameraStatus === "success" && (
|
||||||
|
<select
|
||||||
|
value={selectedVideoDeviceId}
|
||||||
|
onChange={handleVideoDeviceChange}
|
||||||
|
className="bg-white border border-[#DAE0E5] w-full h-10 px-2 py-2.5 rounded-lg text-sm outline-none max-sm:w-full"
|
||||||
|
>
|
||||||
|
{videoDevices.map((device) => (
|
||||||
|
<option key={device.deviceId} value={device.deviceId}>
|
||||||
|
{getDeviceLabel(
|
||||||
|
device,
|
||||||
|
"setName.defaultCamera",
|
||||||
|
"setName.cameraDevice"
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
+22
@@ -199,6 +199,17 @@ const resources = {
|
|||||||
name: "Имя",
|
name: "Имя",
|
||||||
skip: "Не указывать",
|
skip: "Не указывать",
|
||||||
continue: "Продолжить",
|
continue: "Продолжить",
|
||||||
|
micChecking: "Проверка микрофона...",
|
||||||
|
micSuccess: "Микрофон подключен",
|
||||||
|
micError: "Микрофон недоступен",
|
||||||
|
retry: "Повторить",
|
||||||
|
micDevice: "Микрофон",
|
||||||
|
defaultMic: "Микрофон по умолчанию",
|
||||||
|
cameraChecking: "Проверка камеры...",
|
||||||
|
cameraSuccess: "Камера подключена",
|
||||||
|
cameraError: "Камера недоступна",
|
||||||
|
cameraDevice: "Камера",
|
||||||
|
defaultCamera: "Камера по умолчанию",
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
placeholder: "Написать сообщение...",
|
placeholder: "Написать сообщение...",
|
||||||
@@ -427,6 +438,17 @@ const resources = {
|
|||||||
name: "Name",
|
name: "Name",
|
||||||
skip: "Skip",
|
skip: "Skip",
|
||||||
continue: "Continue",
|
continue: "Continue",
|
||||||
|
micChecking: "Checking microphone...",
|
||||||
|
micSuccess: "Microphone connected",
|
||||||
|
micError: "Microphone unavailable",
|
||||||
|
retry: "Retry",
|
||||||
|
micDevice: "Microphone",
|
||||||
|
defaultMic: "Default microphone",
|
||||||
|
cameraChecking: "Checking camera...",
|
||||||
|
cameraSuccess: "Camera connected",
|
||||||
|
cameraError: "Camera unavailable",
|
||||||
|
cameraDevice: "Camera",
|
||||||
|
defaultCamera: "Default camera",
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
placeholder: "Write a message...",
|
placeholder: "Write a message...",
|
||||||
|
|||||||
+166
-54
@@ -80,7 +80,11 @@ function StreamPage() {
|
|||||||
const [roomId] = useState<string>(params.id!);
|
const [roomId] = useState<string>(params.id!);
|
||||||
const { socket, setSocket } = useSocketStore();
|
const { socket, setSocket } = useSocketStore();
|
||||||
const { setModal } = useModalStore();
|
const { setModal } = useModalStore();
|
||||||
const { name } = useStreamStore();
|
const {
|
||||||
|
name,
|
||||||
|
setSelectedAudioDeviceId,
|
||||||
|
setSelectedVideoDeviceId,
|
||||||
|
} = useStreamStore();
|
||||||
const [isMicEnabled, setIsMicEnabled] = useState(true);
|
const [isMicEnabled, setIsMicEnabled] = useState(true);
|
||||||
const [isCameraEnabled, setIsCameraEnabled] = useState(true);
|
const [isCameraEnabled, setIsCameraEnabled] = useState(true);
|
||||||
const [isEnded, setIsEnded] = useState<boolean>();
|
const [isEnded, setIsEnded] = useState<boolean>();
|
||||||
@@ -123,19 +127,77 @@ function StreamPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserMedia() {
|
async function getUserMedia(
|
||||||
|
existingAudioStream?: MediaStream | null,
|
||||||
|
audioDeviceId?: string,
|
||||||
|
existingVideoStream?: MediaStream | null,
|
||||||
|
videoDeviceId?: string
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
let mediaStream: MediaStream;
|
||||||
video: true,
|
|
||||||
audio: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!localVideoRef.current) return;
|
const hasAudio =
|
||||||
|
existingAudioStream && existingAudioStream.getAudioTracks().length;
|
||||||
|
const hasVideo =
|
||||||
|
existingVideoStream && existingVideoStream.getVideoTracks().length;
|
||||||
|
|
||||||
localVideoRef.current.srcObject = mediaStream;
|
if (hasAudio && hasVideo) {
|
||||||
localVideoRef.current.onloadedmetadata = () => {
|
mediaStream = new MediaStream([
|
||||||
localVideoRef.current?.play();
|
...existingAudioStream!.getAudioTracks(),
|
||||||
};
|
...existingVideoStream!.getVideoTracks(),
|
||||||
|
]);
|
||||||
|
} else if (hasAudio) {
|
||||||
|
try {
|
||||||
|
const videoStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: videoDeviceId
|
||||||
|
? { deviceId: { exact: videoDeviceId } }
|
||||||
|
: true,
|
||||||
|
});
|
||||||
|
mediaStream = new MediaStream([
|
||||||
|
...existingAudioStream!.getAudioTracks(),
|
||||||
|
...videoStream.getVideoTracks(),
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
mediaStream = new MediaStream(
|
||||||
|
existingAudioStream!.getAudioTracks()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (hasVideo) {
|
||||||
|
try {
|
||||||
|
const audioStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: audioDeviceId
|
||||||
|
? { deviceId: { exact: audioDeviceId } }
|
||||||
|
: true,
|
||||||
|
});
|
||||||
|
mediaStream = new MediaStream([
|
||||||
|
...audioStream.getAudioTracks(),
|
||||||
|
...existingVideoStream!.getVideoTracks(),
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
mediaStream = new MediaStream(
|
||||||
|
existingVideoStream!.getVideoTracks()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: videoDeviceId
|
||||||
|
? { deviceId: { exact: videoDeviceId } }
|
||||||
|
: true,
|
||||||
|
audio: audioDeviceId
|
||||||
|
? { deviceId: { exact: audioDeviceId } }
|
||||||
|
: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
localVideoRef.current &&
|
||||||
|
mediaStream.getVideoTracks().length > 0
|
||||||
|
) {
|
||||||
|
localVideoRef.current.srcObject = mediaStream;
|
||||||
|
localVideoRef.current.onloadedmetadata = () => {
|
||||||
|
localVideoRef.current?.play();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
setLocalStream(mediaStream);
|
setLocalStream(mediaStream);
|
||||||
setPermission(true);
|
setPermission(true);
|
||||||
@@ -290,9 +352,21 @@ function StreamPage() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSetName() {
|
function handleSetName(
|
||||||
|
audioStream: MediaStream | null,
|
||||||
|
selectedAudioDeviceId: string,
|
||||||
|
videoStream: MediaStream | null,
|
||||||
|
selectedVideoDeviceId: string
|
||||||
|
) {
|
||||||
|
setSelectedAudioDeviceId(selectedAudioDeviceId);
|
||||||
|
setSelectedVideoDeviceId(selectedVideoDeviceId);
|
||||||
setStep(2);
|
setStep(2);
|
||||||
getUserMedia();
|
getUserMedia(
|
||||||
|
audioStream,
|
||||||
|
selectedAudioDeviceId || undefined,
|
||||||
|
videoStream,
|
||||||
|
selectedVideoDeviceId || undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWSUrl() {
|
async function getWSUrl() {
|
||||||
@@ -420,6 +494,15 @@ function StreamPage() {
|
|||||||
}, [users.length]);
|
}, [users.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
localVideoRef.current &&
|
||||||
|
localStream.getVideoTracks().length > 0
|
||||||
|
) {
|
||||||
|
localVideoRef.current.srcObject = localStream;
|
||||||
|
localVideoRef.current.onloadedmetadata = () => {
|
||||||
|
localVideoRef.current?.play();
|
||||||
|
};
|
||||||
|
}
|
||||||
toggleCamera();
|
toggleCamera();
|
||||||
toggleMic();
|
toggleMic();
|
||||||
}, [localStream]);
|
}, [localStream]);
|
||||||
@@ -505,44 +588,52 @@ function StreamPage() {
|
|||||||
|
|
||||||
{permission && (
|
{permission && (
|
||||||
<>
|
<>
|
||||||
<div className="relative group">
|
{localStream.getAudioTracks().length > 0 && (
|
||||||
<Button
|
<div className="relative group">
|
||||||
variant="secondary"
|
<Button
|
||||||
icon={
|
variant="secondary"
|
||||||
isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />
|
icon={
|
||||||
}
|
isMicEnabled ? (
|
||||||
onlyIcon
|
<MicroOnIcon />
|
||||||
onClick={toggleMic}
|
) : (
|
||||||
/>
|
<MicroOffIcon />
|
||||||
<Tooltip
|
)
|
||||||
text={
|
}
|
||||||
isMicEnabled
|
onlyIcon
|
||||||
? t("tooltips.turnOffMic")
|
onClick={toggleMic}
|
||||||
: t("tooltips.turnOnMic")
|
/>
|
||||||
}
|
<Tooltip
|
||||||
/>
|
text={
|
||||||
</div>
|
isMicEnabled
|
||||||
<div className="relative group">
|
? t("tooltips.turnOffMic")
|
||||||
<Button
|
: t("tooltips.turnOnMic")
|
||||||
variant="secondary"
|
}
|
||||||
icon={
|
/>
|
||||||
isCameraEnabled ? (
|
</div>
|
||||||
<CameraOnIcon />
|
)}
|
||||||
) : (
|
{localStream.getVideoTracks().length > 0 && (
|
||||||
<CameraOffIcon />
|
<div className="relative group">
|
||||||
)
|
<Button
|
||||||
}
|
variant="secondary"
|
||||||
onlyIcon
|
icon={
|
||||||
onClick={toggleCamera}
|
isCameraEnabled ? (
|
||||||
/>
|
<CameraOnIcon />
|
||||||
<Tooltip
|
) : (
|
||||||
text={
|
<CameraOffIcon />
|
||||||
isCameraEnabled
|
)
|
||||||
? t("tooltips.turnOffCamera")
|
}
|
||||||
: t("tooltips.turnOnCamera")
|
onlyIcon
|
||||||
}
|
onClick={toggleCamera}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Tooltip
|
||||||
|
text={
|
||||||
|
isCameraEnabled
|
||||||
|
? t("tooltips.turnOffCamera")
|
||||||
|
: t("tooltips.turnOnCamera")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -674,7 +765,12 @@ function StreamPage() {
|
|||||||
<div className="absolute top-2 space-y-2 lg:left-2 max-lg:right-2">
|
<div className="absolute top-2 space-y-2 lg:left-2 max-lg:right-2">
|
||||||
<div
|
<div
|
||||||
className={`relative border-2 rounded-lg ${
|
className={`relative border-2 rounded-lg ${
|
||||||
!permission || !isCameraEnabled ? "hidden" : ""
|
!permission ? "hidden" : ""
|
||||||
|
} ${
|
||||||
|
localStream.getVideoTracks().length === 0 ||
|
||||||
|
!isCameraEnabled
|
||||||
|
? "h-8 overflow-hidden"
|
||||||
|
: ""
|
||||||
} ${
|
} ${
|
||||||
isMicEnabled && isSpeaking
|
isMicEnabled && isSpeaking
|
||||||
? "border-green-500"
|
? "border-green-500"
|
||||||
@@ -683,12 +779,28 @@ function StreamPage() {
|
|||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
ref={localVideoRef}
|
ref={localVideoRef}
|
||||||
className={`object-cover bg-gray-500 rounded-lg aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] -scale-x-100`}
|
className={`object-cover bg-gray-500 rounded-lg aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] -scale-x-100 ${
|
||||||
|
localStream.getVideoTracks().length === 0 ||
|
||||||
|
!isCameraEnabled
|
||||||
|
? "hidden"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
playsInline
|
playsInline
|
||||||
autoPlay
|
autoPlay
|
||||||
muted
|
muted
|
||||||
></video>
|
></video>
|
||||||
<div className="absolute bottom-0 p-2">
|
{(localStream.getVideoTracks().length === 0 ||
|
||||||
|
!isCameraEnabled) && (
|
||||||
|
<div className="aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg bg-gray-500" />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`absolute -bottom-1.5 flex items-center lg:w-[216px] w-[160px] gap-2 p-2 ${
|
||||||
|
localStream.getVideoTracks().length === 0 ||
|
||||||
|
!isCameraEnabled
|
||||||
|
? "bg-black"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<p className="text-xs text-white truncate lg:text-sm">
|
<p className="text-xs text-white truncate lg:text-sm">
|
||||||
{name}
|
{name}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import { devtools, persist } from "zustand/middleware";
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
name: string;
|
name: string;
|
||||||
|
selectedAudioDeviceId: string;
|
||||||
|
selectedVideoDeviceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Actions {
|
interface Actions {
|
||||||
setName: (name: string) => void;
|
setName: (name: string) => void;
|
||||||
|
setSelectedAudioDeviceId: (id: string) => void;
|
||||||
|
setSelectedVideoDeviceId: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStreamStore = create<State & Actions>()(
|
const useStreamStore = create<State & Actions>()(
|
||||||
@@ -14,7 +18,11 @@ const useStreamStore = create<State & Actions>()(
|
|||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
name: "",
|
name: "",
|
||||||
|
selectedAudioDeviceId: "",
|
||||||
|
selectedVideoDeviceId: "",
|
||||||
setName: (name) => set({ name }),
|
setName: (name) => set({ name }),
|
||||||
|
setSelectedAudioDeviceId: (id) => set({ selectedAudioDeviceId: id }),
|
||||||
|
setSelectedVideoDeviceId: (id) => set({ selectedVideoDeviceId: id }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "auth",
|
name: "auth",
|
||||||
|
|||||||
Reference in New Issue
Block a user