Compare commits
2 Commits
42082faf0d
...
c2fc1624a4
| Author | SHA1 | Date | |
|---|---|---|---|
| c2fc1624a4 | |||
| cb156bd99d |
+1
-1
@@ -507,7 +507,7 @@ function App() {
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-xs text-[#C5C7CE]">
|
||||
© 2023 GRAFF interactive.{" "}
|
||||
© 2026 GRAFF interactive.{" "}
|
||||
<Trans i18nKey={"footer.text"}>Все права защищены.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+68
-25
@@ -13,51 +13,94 @@ interface Props {
|
||||
user?: IUser;
|
||||
}
|
||||
|
||||
const SPEAKING_HIDE_DELAY_MS = 400;
|
||||
|
||||
function Video({ mediaStream, muted, user }: Props) {
|
||||
const remoteVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const remoteAudioRef = useRef<HTMLAudioElement>(null);
|
||||
const isSpeaking = useIsAudioActive({ source: mediaStream });
|
||||
const [showSpeakingBorder, setShowSpeakingBorder] = useState(false);
|
||||
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [_muted, setMuted] = useState(muted);
|
||||
const [isLoading, setIsLoading] = useState(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() {
|
||||
if (!remoteVideoRef.current) return;
|
||||
// remoteVideoRef.current.muted = !remoteVideoRef.current.muted;
|
||||
setMuted((prev) => !prev);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!remoteVideoRef.current) return;
|
||||
if (!mediaStream) return;
|
||||
|
||||
remoteVideoRef.current.srcObject = mediaStream;
|
||||
remoteVideoRef.current.onloadedmetadata = () => {
|
||||
remoteVideoRef.current?.play();
|
||||
};
|
||||
|
||||
remoteVideoRef.current.onplay = () => {
|
||||
if (hasVideo && remoteVideoRef.current) {
|
||||
remoteVideoRef.current.srcObject = mediaStream;
|
||||
remoteVideoRef.current.onloadedmetadata = () => {
|
||||
remoteVideoRef.current?.play();
|
||||
};
|
||||
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);
|
||||
};
|
||||
|
||||
console.log("mediaStream", mediaStream?.getTracks());
|
||||
}, [mediaStream]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("remoteVideoRef.current", remoteVideoRef);
|
||||
}, [remoteVideoRef.current]);
|
||||
}
|
||||
}, [mediaStream, hasVideo]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative border-2 rounded-lg ${
|
||||
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
|
||||
ref={remoteVideoRef}
|
||||
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500`}
|
||||
playsInline
|
||||
autoPlay
|
||||
muted={_muted}
|
||||
></video>
|
||||
{hasVideo ? (
|
||||
<video
|
||||
ref={remoteVideoRef}
|
||||
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500`}
|
||||
playsInline
|
||||
autoPlay
|
||||
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
|
||||
className={`absolute -bottom-1.5 flex items-center justify-between w-full gap-2 p-2 ${
|
||||
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 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<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>) {
|
||||
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<HTMLFormElement>) {
|
||||
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 (
|
||||
<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">
|
||||
<Trans i18nKey={"setName.hello"}>Здравствуйте!</Trans>
|
||||
</p>
|
||||
@@ -57,9 +257,132 @@ function SetNameModal({ onAction }: Props) {
|
||||
onChange={handleChangeName}
|
||||
autoFocus={!name}
|
||||
required
|
||||
className="max-sm:w-full"
|
||||
className="!w-full"
|
||||
/>
|
||||
</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">
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
+22
@@ -199,6 +199,17 @@ const resources = {
|
||||
name: "Имя",
|
||||
skip: "Не указывать",
|
||||
continue: "Продолжить",
|
||||
micChecking: "Проверка микрофона...",
|
||||
micSuccess: "Микрофон подключен",
|
||||
micError: "Микрофон недоступен",
|
||||
retry: "Повторить",
|
||||
micDevice: "Микрофон",
|
||||
defaultMic: "Микрофон по умолчанию",
|
||||
cameraChecking: "Проверка камеры...",
|
||||
cameraSuccess: "Камера подключена",
|
||||
cameraError: "Камера недоступна",
|
||||
cameraDevice: "Камера",
|
||||
defaultCamera: "Камера по умолчанию",
|
||||
},
|
||||
chat: {
|
||||
placeholder: "Написать сообщение...",
|
||||
@@ -427,6 +438,17 @@ const resources = {
|
||||
name: "Name",
|
||||
skip: "Skip",
|
||||
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: {
|
||||
placeholder: "Write a message...",
|
||||
|
||||
+166
-54
@@ -80,7 +80,11 @@ function StreamPage() {
|
||||
const [roomId] = useState<string>(params.id!);
|
||||
const { socket, setSocket } = useSocketStore();
|
||||
const { setModal } = useModalStore();
|
||||
const { name } = useStreamStore();
|
||||
const {
|
||||
name,
|
||||
setSelectedAudioDeviceId,
|
||||
setSelectedVideoDeviceId,
|
||||
} = useStreamStore();
|
||||
const [isMicEnabled, setIsMicEnabled] = useState(true);
|
||||
const [isCameraEnabled, setIsCameraEnabled] = useState(true);
|
||||
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 {
|
||||
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: true,
|
||||
});
|
||||
let mediaStream: MediaStream;
|
||||
|
||||
if (!localVideoRef.current) return;
|
||||
const hasAudio =
|
||||
existingAudioStream && existingAudioStream.getAudioTracks().length;
|
||||
const hasVideo =
|
||||
existingVideoStream && existingVideoStream.getVideoTracks().length;
|
||||
|
||||
localVideoRef.current.srcObject = mediaStream;
|
||||
localVideoRef.current.onloadedmetadata = () => {
|
||||
localVideoRef.current?.play();
|
||||
};
|
||||
if (hasAudio && hasVideo) {
|
||||
mediaStream = new MediaStream([
|
||||
...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);
|
||||
setPermission(true);
|
||||
@@ -290,9 +352,21 @@ function StreamPage() {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function handleSetName() {
|
||||
function handleSetName(
|
||||
audioStream: MediaStream | null,
|
||||
selectedAudioDeviceId: string,
|
||||
videoStream: MediaStream | null,
|
||||
selectedVideoDeviceId: string
|
||||
) {
|
||||
setSelectedAudioDeviceId(selectedAudioDeviceId);
|
||||
setSelectedVideoDeviceId(selectedVideoDeviceId);
|
||||
setStep(2);
|
||||
getUserMedia();
|
||||
getUserMedia(
|
||||
audioStream,
|
||||
selectedAudioDeviceId || undefined,
|
||||
videoStream,
|
||||
selectedVideoDeviceId || undefined
|
||||
);
|
||||
}
|
||||
|
||||
async function getWSUrl() {
|
||||
@@ -420,6 +494,15 @@ function StreamPage() {
|
||||
}, [users.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
localVideoRef.current &&
|
||||
localStream.getVideoTracks().length > 0
|
||||
) {
|
||||
localVideoRef.current.srcObject = localStream;
|
||||
localVideoRef.current.onloadedmetadata = () => {
|
||||
localVideoRef.current?.play();
|
||||
};
|
||||
}
|
||||
toggleCamera();
|
||||
toggleMic();
|
||||
}, [localStream]);
|
||||
@@ -505,44 +588,52 @@ function StreamPage() {
|
||||
|
||||
{permission && (
|
||||
<>
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={
|
||||
isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />
|
||||
}
|
||||
onlyIcon
|
||||
onClick={toggleMic}
|
||||
/>
|
||||
<Tooltip
|
||||
text={
|
||||
isMicEnabled
|
||||
? t("tooltips.turnOffMic")
|
||||
: t("tooltips.turnOnMic")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={
|
||||
isCameraEnabled ? (
|
||||
<CameraOnIcon />
|
||||
) : (
|
||||
<CameraOffIcon />
|
||||
)
|
||||
}
|
||||
onlyIcon
|
||||
onClick={toggleCamera}
|
||||
/>
|
||||
<Tooltip
|
||||
text={
|
||||
isCameraEnabled
|
||||
? t("tooltips.turnOffCamera")
|
||||
: t("tooltips.turnOnCamera")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{localStream.getAudioTracks().length > 0 && (
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={
|
||||
isMicEnabled ? (
|
||||
<MicroOnIcon />
|
||||
) : (
|
||||
<MicroOffIcon />
|
||||
)
|
||||
}
|
||||
onlyIcon
|
||||
onClick={toggleMic}
|
||||
/>
|
||||
<Tooltip
|
||||
text={
|
||||
isMicEnabled
|
||||
? t("tooltips.turnOffMic")
|
||||
: t("tooltips.turnOnMic")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{localStream.getVideoTracks().length > 0 && (
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={
|
||||
isCameraEnabled ? (
|
||||
<CameraOnIcon />
|
||||
) : (
|
||||
<CameraOffIcon />
|
||||
)
|
||||
}
|
||||
onlyIcon
|
||||
onClick={toggleCamera}
|
||||
/>
|
||||
<Tooltip
|
||||
text={
|
||||
isCameraEnabled
|
||||
? t("tooltips.turnOffCamera")
|
||||
: t("tooltips.turnOnCamera")
|
||||
}
|
||||
/>
|
||||
</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={`relative border-2 rounded-lg ${
|
||||
!permission || !isCameraEnabled ? "hidden" : ""
|
||||
!permission ? "hidden" : ""
|
||||
} ${
|
||||
localStream.getVideoTracks().length === 0 ||
|
||||
!isCameraEnabled
|
||||
? "h-8 overflow-hidden"
|
||||
: ""
|
||||
} ${
|
||||
isMicEnabled && isSpeaking
|
||||
? "border-green-500"
|
||||
@@ -683,12 +779,28 @@ function StreamPage() {
|
||||
>
|
||||
<video
|
||||
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
|
||||
autoPlay
|
||||
muted
|
||||
></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">
|
||||
{name}
|
||||
</p>
|
||||
|
||||
@@ -3,10 +3,14 @@ import { devtools, persist } from "zustand/middleware";
|
||||
|
||||
interface State {
|
||||
name: string;
|
||||
selectedAudioDeviceId: string;
|
||||
selectedVideoDeviceId: string;
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
setName: (name: string) => void;
|
||||
setSelectedAudioDeviceId: (id: string) => void;
|
||||
setSelectedVideoDeviceId: (id: string) => void;
|
||||
}
|
||||
|
||||
const useStreamStore = create<State & Actions>()(
|
||||
@@ -14,7 +18,11 @@ const useStreamStore = create<State & Actions>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
name: "",
|
||||
selectedAudioDeviceId: "",
|
||||
selectedVideoDeviceId: "",
|
||||
setName: (name) => set({ name }),
|
||||
setSelectedAudioDeviceId: (id) => set({ selectedAudioDeviceId: id }),
|
||||
setSelectedVideoDeviceId: (id) => set({ selectedVideoDeviceId: id }),
|
||||
}),
|
||||
{
|
||||
name: "auth",
|
||||
|
||||
Reference in New Issue
Block a user