Skip to content

Commit

Permalink
Merge pull request #129 from Jw705/feature/231122-apply-webrtc-in-header
Browse files Browse the repository at this point in the history
Feature(#34) ๋ฐœํ‘œ์ž ํ—ค๋”๋ฅผ ํ†ตํ•ด ์Œ์„ฑ์„ ๋ฏธ๋””์–ด ์„œ๋ฒ„๋กœ ์ „๋‹ฌ
  • Loading branch information
Jw705 authored Nov 23, 2023
2 parents 88b8854 + db95b00 commit 13324d9
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 162 deletions.
4 changes: 2 additions & 2 deletions frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState } from "react";
import HeaderLogo from "./components/HeaderLogo";
import HeaderLoginButton from "./components/HeaderLoginButton";
import HeaderMainButtons from "./components/HeaderMainButtons";
import HeaderLecturerControls from "./components/HeaderLecturerControls";
import HeaderInstructorControls from "./components/HeaderInstructorControls";
import HeaderProfileButton from "./components/HeaderProfileButton";
import HeaderSettingButton from "./components/HeaderSettingButton";

Expand Down Expand Up @@ -31,7 +31,7 @@ const Header = ({ type }: HeaderProps) => {
)}
{type === "instructor" && (
<>
<HeaderLecturerControls />
<HeaderInstructorControls />
<HeaderSettingButton isSettingClicked={isSettingClicked} setIsSettingClicked={setIsSettingClicked} />
<HeaderProfileButton isProfileClicked={isProfileClicked} setIsProfileClicked={setIsProfileClicked} />
</>
Expand Down
225 changes: 225 additions & 0 deletions frontend/src/components/Header/components/HeaderInstructorControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { useState, useRef } from "react";
import { useRecoilValue } from "recoil";
import { io, Socket } from "socket.io-client";

import VolumeMeter from "./VolumeMeter";

import PlayIcon from "@/assets/svgs/play.svg?react";
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 selectedMicrophoneState from "./stateMicrophone";

const HeaderInstructorControls = () => {
const [isLectureStart, setIsLectureStart] = useState(false);
const [isMicOn, setIsMicOn] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [recordingTime, setRecordingTime] = useState<number>(0);
const [micVolume, setMicVolume] = useState<number>(0);

const recordingTimerRef = 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 selectedMicrophone = useRecoilValue(selectedMicrophoneState);
const MEDIA_SERVER_URL = "http://localhost:3000/create-room";

const startLecture = async () => {
if (!selectedMicrophone) return alert("์Œ์„ฑ ์ž…๋ ฅ์žฅ์น˜(๋งˆ์ดํฌ)๋ฅผ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”");

await initConnection();
await createPresenterOffer();
listenForServerAnswer();
};

const stopLecture = () => {
if (!isLectureStart) return alert("๊ฐ•์˜๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.");

setIsLectureStart(false);
setRecordingTime(0);

if (recordingTimerRef.current) clearInterval(recordingTimerRef.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); // ์ผ๋‹จ์€ ๋ชจ๋‹ฌ๋งŒ ๋‹ซ์Šต๋‹ˆ๋‹ค.
};

const initConnection = async () => {
try {
// 0. ์†Œ์ผ“ ์—ฐ๊ฒฐ
socketRef.current = io(MEDIA_SERVER_URL);

// 1. ๋กœ์ปฌ stream ์ƒ์„ฑ (๋ฐœํ‘œ์ž ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฏธ๋””์–ด track ์„ค์ •)
if (!selectedMicrophone) throw new Error("๋งˆ์ดํฌ๋ฅผ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”");
const stream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: selectedMicrophone }
});
mediaStreamRef.current = stream;
console.log("1. ๋กœ์ปฌ stream ์ƒ์„ฑ ์™„๋ฃŒ");

setIsLectureStart(true);
setupAudioAnalysis(stream);
startRecordingTimer();

// 2. ๋กœ์ปฌ RTCPeerConnection ์ƒ์„ฑ
pcRef.current = new RTCPeerConnection();
console.log("2. ๋กœ์ปฌ RTCPeerConnection ์ƒ์„ฑ ์™„๋ฃŒ");

// 3. ๋กœ์ปฌ stream์— track ์ถ”๊ฐ€, ๋ฐœํ‘œ์ž์˜ ๋ฏธ๋””์–ด ํŠธ๋ž™์„ ๋กœ์ปฌ RTCPeerConnection์— ์ถ”๊ฐ€
if (stream) {
console.log(stream);
console.log("3.track ์ถ”๊ฐ€");
stream.getTracks().forEach((track) => {
console.log("track:", track);
if (!pcRef.current) return;
pcRef.current.addTrack(track, stream);
});
} else {
console.error("no stream");
}
} catch (e) {
console.error(e);
}
};

async function createPresenterOffer() {
// 4. ๋ฐœํ‘œ์ž์˜ offer ์ƒ์„ฑ
try {
if (!pcRef.current || !socketRef.current) return;
const SDP = await pcRef.current.createOffer();
socketRef.current.emit("presenterOffer", {
socketId: socketRef.current.id,
SDP: SDP
});
console.log("4. ๋ฐœํ‘œ์ž localDescription ์„ค์ • ์™„๋ฃŒ");
pcRef.current.setLocalDescription(SDP);
getPresenterCandidate();
} catch (e) {
console.error(e);
}
}

function getPresenterCandidate() {
// 5. ๋ฐœํ‘œ์ž์˜ candidate ์ˆ˜์ง‘
if (!pcRef.current) return;
pcRef.current.onicecandidate = (e) => {
if (e.candidate) {
if (!socketRef.current) return;
console.log("5. ๋ฐœํ‘œ์ž candidate ์ˆ˜์ง‘");
socketRef.current.emit("presenterCandidate", {
candidate: e.candidate,
presenterSocketId: socketRef.current.id
});
}
};
}

async function listenForServerAnswer() {
// 6. ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ answer ๋ฐ›์Œ
if (!socketRef.current) return;
socketRef.current.on(`${socketRef.current.id}-serverAnswer`, (data) => {
if (!pcRef.current) return;
console.log("6. remoteDescription ์„ค์ •์™„๋ฃŒ");
pcRef.current.setRemoteDescription(data.SDP);
});
socketRef.current.on(`${socketRef.current.id}-serverCandidate`, (data) => {
if (!pcRef.current) return;
console.log("7. ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ candidate ๋ฐ›์Œ");
pcRef.current.addIceCandidate(new RTCIceCandidate(data.candidate));
});
}

// ๋งˆ์ดํฌ ๋ณผ๋ฅจ ์ธก์ •์„ ์œ„ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค
const setupAudioAnalysis = (stream: MediaStream) => {
const context = new AudioContext();
const analyser = context.createAnalyser();
const mediaStreamAudioSourceNode = context.createMediaStreamSource(stream);
mediaStreamAudioSourceNode.connect(analyser, 0);
const pcmData = new Float32Array(analyser.fftSize);

const onFrame = () => {
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 startRecordingTimer = () => {
let startTime = Date.now();
const updateRecordingTime = () => {
const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
setRecordingTime(elapsedTime);
};
const recordingTimer = setInterval(updateRecordingTime, 1000);
recordingTimerRef.current = recordingTimer;
};

return (
<>
<div className="flex gap-2 fixed left-1/2 -translate-x-1/2">
<VolumeMeter micVolume={micVolume} />
<p className="semibold-20 text-boarlog-100">
{Math.floor(recordingTime / 60)
.toString()
.padStart(2, "0")}
:{(recordingTime % 60).toString().padStart(2, "0")}
</p>
</div>

<SmallButton
className={`text-grayscale-white ${isLectureStart ? "bg-alert-100" : "bg-boarlog-100"}`}
onClick={!isLectureStart ? startLecture : () => setIsModalOpen(true)}
>
{isLectureStart ? (
<>
<StopIcon className="w-5 h-5 fill-grayscale-white" />
๊ฐ•์˜ ์ข…๋ฃŒ
</>
) : (
<>
<PlayIcon className="w-5 h-5 fill-grayscale-white" />
๊ฐ•์˜ ์‹œ์ž‘
</>
)}
</SmallButton>
<SmallButton
className={`text-grayscale-white ${isMicOn ? "bg-boarlog-100" : "bg-alert-100"}`}
onClick={() => setIsMicOn(!isMicOn)}
>
{isMicOn ? (
<MicOnIcon className="w-5 h-5 fill-grayscale-white" />
) : (
<MicOffIcon className="w-5 h-5 fill-grayscale-white" />
)}
</SmallButton>
<Modal
modalText="๊ฐ•์˜๋ฅผ ์ข…๋ฃŒํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?"
cancelText="์ทจ์†Œ"
confirmText="๊ฐ•์˜ ์ข…๋ฃŒํ•˜๊ธฐ"
cancelButtonStyle="black"
confirmButtonStyle="red"
confirmClick={stopLecture}
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
/>
</>
);
};

export default HeaderInstructorControls;
155 changes: 0 additions & 155 deletions frontend/src/components/Header/components/HeaderLecturerControls.tsx

This file was deleted.

Loading

0 comments on commit 13324d9

Please sign in to comment.