Skip to content

Commit

Permalink
Merge pull request #159 from Jw705/feature/231127-participant-connectโ€ฆ
Browse files Browse the repository at this point in the history
โ€ฆ-to-server

Feat(#58,#71,#72) ์ฐธ์—ฌ์ž ํŽ˜์ด์ง€ ์Œ์„ฑ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ตฌํ˜„, ์Œ๋Ÿ‰ ๋ฐ ์Šคํ”ผ์ปค ์กฐ์ ˆ
  • Loading branch information
Jw705 authored Nov 29, 2023
2 parents 24e7688 + 0335bdc commit 80cc073
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 51 deletions.
3 changes: 3 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?


.env
12 changes: 12 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"dotenv": "^16.3.1",
"fabric": "^5.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import MicOnIcon from "@/assets/svgs/micOn.svg?react";
import MicOffIcon from "@/assets/svgs/micOff.svg?react";
import SmallButton from "@/components/SmallButton/SmallButton";
import Modal from "@/components/Modal/Modal";
import { useToast } from "@/components/Toast/useToast";

import selectedMicrophoneState from "./stateMicrophone";
import selectedMicrophoneState from "./stateSelectedMicrophone";
import micVolmeState from "./stateMicVolme";

const HeaderInstructorControls = () => {
Expand All @@ -24,6 +25,7 @@ const HeaderInstructorControls = () => {
const selectedMicrophone = useRecoilValue(selectedMicrophoneState);
const inputMicVolume = useRecoilValue(micVolmeState);
const setInputMicVolumeState = useSetRecoilState(micVolmeState);
const showToast = useToast();

const timerIdRef = useRef<number | null>(null); // ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ํ‘œ์‹œ ํƒ€์ด๋จธ id
const onFrameIdRef = useRef<number | null>(null); // ๋งˆ์ดํฌ ๋ณผ๋ฅจ ์ธก์ • ํƒ€์ด๋จธ id
Expand All @@ -34,6 +36,18 @@ const HeaderInstructorControls = () => {
const inputMicVolumeRef = useRef<number>(0);
const prevInputMicVolumeRef = useRef<number>(0);
const MEDIA_SERVER_URL = "http://localhost:3000/create-room";
const pc_config = {
iceServers: [
{
urls: ["stun:stun.l.google.com:19302"]
},
{
urls: import.meta.env.VITE_TURN_URL as string,
username: import.meta.env.VITE_TURN_USERNAME as string,
credential: import.meta.env.VITE_TURN_PASSWORD as string
}
]
};

useEffect(() => {
inputMicVolumeRef.current = inputMicVolume;
Expand Down Expand Up @@ -84,7 +98,7 @@ const HeaderInstructorControls = () => {
startTimer();

// 2. ๋กœ์ปฌ RTCPeerConnection ์ƒ์„ฑ
pcRef.current = new RTCPeerConnection();
pcRef.current = new RTCPeerConnection(pc_config);
// 3. ๋กœ์ปฌ stream์— track ์ถ”๊ฐ€, ๋ฐœํ‘œ์ž์˜ ๋ฏธ๋””์–ด ํŠธ๋ž™์„ ๋กœ์ปฌ RTCPeerConnection์— ์ถ”๊ฐ€
if (updatedStreamRef.current) {
updatedStreamRef.current.getTracks().forEach((track) => {
Expand Down Expand Up @@ -190,7 +204,7 @@ const HeaderInstructorControls = () => {

// ๊ฒฝ๊ณผ ์‹œ๊ฐ„์„ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค
const startTimer = () => {
let startTime = Date.now();
const startTime = Date.now();
const updateElapsedTime = () => {
const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
setElapsedTime(elapsedTime);
Expand Down Expand Up @@ -225,9 +239,11 @@ const HeaderInstructorControls = () => {
prevInputMicVolumeRef.current = inputMicVolumeRef.current;
setInputMicVolumeState(0);
setIsMicOn(false);
showToast({ message: "๋งˆ์ดํฌ ์Œ์†Œ๊ฑฐ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", type: "alert" });
} else {
setInputMicVolumeState(prevInputMicVolumeRef.current);
setIsMicOn(true);
showToast({ message: "๋งˆ์ดํฌ ์Œ์†Œ๊ฑฐ๊ฐ€ ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", type: "success" });
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useSetRecoilState } from "recoil";
import selectedMicrophoneState from "./stateMicrophone";
import selectedMicrophoneState from "./stateSelectedMicrophone";
import micVolmeState from "./stateMicVolme";

interface HeaderSettingModalProps {
Expand Down Expand Up @@ -40,13 +40,13 @@ const HeaderSettingModal = ({ isSettingClicked, setIsSettingClicked }: HeaderSet
isSettingClicked ? "opacity-100 visible" : "opacity-0 invisible"
}`}
>
<div className="flex flex-row gap-3 w-full h-10 justify-start">
<div className="flex flex-row gap-3 w-full justify-start">
<p id="input-device-label">๋งˆ์ดํฌ ์„ ํƒ</p>
</div>

<select
aria-labelledby="input-device-label"
className="border w-full"
className="border w-full rounded-xl px-3 py-4"
onChange={(e) => setSelectedMicrophone(e.target.value)}
>
{microphoneDevices.map((device) => (
Expand Down
197 changes: 166 additions & 31 deletions frontend/src/components/Header/components/HeaderParticipantControls.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,202 @@
import { useState, useRef, useEffect } from "react";
// @ts-ignore
import { useRecoilValue, useSetRecoilState } from "recoil";
//import { io, Socket } from "socket.io-client";
import { io, Socket } from "socket.io-client";
import { useNavigate } from "react-router-dom";

import VolumeMeter from "./VolumeMeter";

import StopIcon from "@/assets/svgs/stop.svg?react";
import MicOnIcon from "@/assets/svgs/micOn.svg?react";
import MicOffIcon from "@/assets/svgs/micOff.svg?react";
import SmallButton from "@/components/SmallButton/SmallButton";
import Modal from "@/components/Modal/Modal";
import { useToast } from "@/components/Toast/useToast";

import selectedMicrophoneState from "./stateMicrophone";
import micVolmeState from "./stateMicVolme";
import selectedSpeakerState from "./stateSelectedSpeaker";
import speakerVolmeState from "./stateSpeakerVolme";

const HeaderParticipantControls = () => {
// @ts-ignore
const [isLectureStart, setIsLectureStart] = useState(false);
const [isSpeakerOn, setisSpeakerOn] = useState(true);
const [isSpeakerOn, setisSpeakerOn] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
// @ts-ignore
const [elapsedTime, setElapsedTime] = useState<number>(0);
// @ts-ignore
const [micVolume, setMicVolume] = useState<number>(0);

const selectedMicrophone = useRecoilValue(selectedMicrophoneState);
const SpeakerVolume = useRecoilValue(micVolmeState);
//const setSpeakerVolumeState = useSetRecoilState(micVolmeState);
const [didMount, setDidMount] = useState(false);

const selectedSpeaker = useRecoilValue(selectedSpeakerState);
const SpeakerVolume = useRecoilValue(speakerVolmeState);
const setSpeakerVolume = useSetRecoilState(speakerVolmeState);

// ์•„๋ž˜๋Š” ์ถ”ํ›„์— ์‚ฌ์šฉํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.
//const timerIdRef = useRef<number | null>(null); // ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ํ‘œ์‹œ ํƒ€์ด๋จธ id
//const onFrameIdRef = useRef<number | null>(null); // ๋งˆ์ดํฌ ๋ณผ๋ฅจ ์ธก์ • ํƒ€์ด๋จธ id
//const socketRef = useRef<Socket>();
//const pcRef = useRef<RTCPeerConnection>();
//const mediaStreamRef = useRef<MediaStream>();
//const updatedStreamRef = useRef<MediaStream>();
const SpeakerVolumeRef = useRef<number>(0);
//const prevSpeakerVolumeRef = useRef<number>(0);
const timerIdRef = useRef<number | null>(null); // ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ํ‘œ์‹œ ํƒ€์ด๋จธ id
const onFrameIdRef = useRef<number | null>(null); // ๋งˆ์ดํฌ ๋ณผ๋ฅจ ์ธก์ • ํƒ€์ด๋จธ id
const socketRef = useRef<Socket>();
const pcRef = useRef<RTCPeerConnection>();
const mediaStreamRef = useRef<MediaStream>();
const localAudioRef = useRef<HTMLAudioElement>(null);
const speakerVolumeRef = useRef<number>(0);
const prevSpeakerVolumeRef = useRef<number>(0);
const audioContextRef = useRef<AudioContext | null>(null);

const navigate = useNavigate();
//const MEDIA_SERVER_URL = "http://localhost:3000/create-room";
const showToast = useToast();
const MEDIA_SERVER_URL = "http://localhost:3000/enter-room";
const pc_config = {
iceServers: [
{
urls: ["stun:stun.l.google.com:19302"]
},
{
urls: import.meta.env.VITE_TURN_URL as string,
username: import.meta.env.VITE_TURN_USERNAME as string,
credential: import.meta.env.VITE_TURN_PASSWORD as string
}
]
};

useEffect(() => {
SpeakerVolumeRef.current = SpeakerVolume;
}, [SpeakerVolume]);
setDidMount(true);
}, []);
useEffect(() => {
if (isLectureStart) {
// ์ถ”ํ›„ ๊ตฌํ˜„
if (didMount) {
enterLecture();
}
}, [selectedMicrophone]);
}, [didMount]);

useEffect(() => {
speakerVolumeRef.current = SpeakerVolume;
}, [SpeakerVolume]);
useEffect(() => {
if (!audioContextRef.current) return;
(audioContextRef.current as any).setSinkId(selectedSpeaker);
}, [selectedSpeaker]);

const enterLecture = async () => {
await initConnection();

await createStudentOffer();
await setServerAnswer();
showToast({ message: "์Œ์†Œ๊ฑฐ ํ•ด์ œ ํ›„ ์†Œ๋ฆฌ๋ฅผ ๋“ค์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค", type: "alert" });
};

const leaveLecture = () => {
setElapsedTime(0);

if (timerIdRef.current) clearInterval(timerIdRef.current); // ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ํ‘œ์‹œ ํƒ€์ด๋จธ ์ค‘์ง€
if (onFrameIdRef.current) window.cancelAnimationFrame(onFrameIdRef.current); // ๋งˆ์ดํฌ ๋ณผ๋ฅจ ์ธก์ • ์ค‘์ง€
if (socketRef.current) socketRef.current.disconnect(); // ์†Œ์ผ“ ์—ฐ๊ฒฐ ํ•ด์ œ
if (pcRef.current) pcRef.current.close(); // RTCPeerConnection ํ•ด์ œ
if (mediaStreamRef.current) mediaStreamRef.current.getTracks().forEach((track) => track.stop()); // ๋ฏธ๋””์–ด ํŠธ๋ž™ ์ค‘์ง€

setIsModalOpen(false);
navigate("/");
};

const initConnection = async () => {
try {
socketRef.current = io(MEDIA_SERVER_URL);
pcRef.current = new RTCPeerConnection(pc_config);
const stream = new MediaStream();
mediaStreamRef.current = stream;

if (!pcRef.current) return;
pcRef.current.ontrack = (event) => {
if (!mediaStreamRef.current || !localAudioRef.current) return;
if (event.track.kind === "audio") {
mediaStreamRef.current.addTrack(event.track);
localAudioRef.current.srcObject = mediaStreamRef.current;
}
};
} catch (e) {
console.error("์—ฐ๊ฒฐ ์—๋Ÿฌ", e);
}
};

function getStudentCandidate() {
if (!pcRef.current) return;
pcRef.current.onicecandidate = (e) => {
if (e.candidate) {
if (!socketRef.current) return;
socketRef.current.emit("studentCandidate", {
candidate: e.candidate,
studentSocketId: socketRef.current.id
});
}
};
}

async function createStudentOffer() {
try {
if (!pcRef.current || !socketRef.current) return;
const SDP = await pcRef.current.createOffer({
offerToReceiveAudio: true
});
socketRef.current.emit("studentOffer", {
socketId: socketRef.current.id,
SDP: SDP
});

pcRef.current.setLocalDescription(SDP);
getStudentCandidate();
} catch (e) {
console.log(e);
}
}

async function setServerAnswer() {
if (!socketRef.current) return;
socketRef.current.on(`${socketRef.current.id}-serverAnswer`, (data) => {
if (!pcRef.current) return;
pcRef.current.setRemoteDescription(data.SDP);
});
socketRef.current.on(`${socketRef.current.id}-serverCandidate`, (data) => {
if (!pcRef.current) return;
pcRef.current.addIceCandidate(new RTCIceCandidate(data.candidate));
});
}

const startAnalyse = () => {
if (!mediaStreamRef.current) return;
audioContextRef.current = new AudioContext();
const analyser = audioContextRef.current.createAnalyser();
const destination = audioContextRef.current.destination;
const mediaStreamAudioSourceNode = audioContextRef.current.createMediaStreamSource(mediaStreamRef.current);

const gainNode = audioContextRef.current.createGain();
mediaStreamAudioSourceNode.connect(gainNode);
gainNode.connect(analyser);
gainNode.connect(destination);

const pcmData = new Float32Array(analyser.fftSize);

const onFrame = () => {
gainNode.gain.value = speakerVolumeRef.current;
analyser.getFloatTimeDomainData(pcmData);
let sum = 0.0;
for (const amplitude of pcmData) {
sum += amplitude * amplitude;
}
const rms = Math.sqrt(sum / pcmData.length);
const normalizedVolume = Math.min(1, rms / 0.5);
setMicVolume(normalizedVolume);
onFrameIdRef.current = window.requestAnimationFrame(onFrame);
};
onFrameIdRef.current = window.requestAnimationFrame(onFrame);
};

const mute = () => {
if (isSpeakerOn) {
// ์ถ”ํ›„ ๊ตฌํ˜„
if (!onFrameIdRef.current) {
// ์ตœ์ดˆ ์—ฐ๊ฒฐ ํ›„ ์Œ์†Œ๊ฑฐ ํ•ด์ œ
startAnalyse();
setisSpeakerOn(true);
showToast({ message: "์Œ์†Œ๊ฑฐ๊ฐ€ ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", type: "success" });
} else if (isSpeakerOn) {
prevSpeakerVolumeRef.current = speakerVolumeRef.current;
setSpeakerVolume(0);
setisSpeakerOn(false);
showToast({ message: "์Œ์†Œ๊ฑฐ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", type: "alert" });
} else {
// ์ถ”ํ›„ ๊ตฌํ˜„
setSpeakerVolume(prevSpeakerVolumeRef.current);
setisSpeakerOn(true);
showToast({ message: "์Œ์†Œ๊ฑฐ๊ฐ€ ํ•ด์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", type: "success" });
}
};

Expand Down Expand Up @@ -99,6 +233,7 @@ const HeaderParticipantControls = () => {
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
/>
<audio id="localAudio" playsInline autoPlay muted ref={localAudioRef}></audio>
</>
);
};
Expand Down
Loading

0 comments on commit 80cc073

Please sign in to comment.