-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #129 from Jw705/feature/231122-apply-webrtc-in-header
Feature(#34) ๋ฐํ์ ํค๋๋ฅผ ํตํด ์์ฑ์ ๋ฏธ๋์ด ์๋ฒ๋ก ์ ๋ฌ
- Loading branch information
Showing
6 changed files
with
241 additions
and
162 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
225 changes: 225 additions & 0 deletions
225
frontend/src/components/Header/components/HeaderInstructorControls.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
155
frontend/src/components/Header/components/HeaderLecturerControls.tsx
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.