Skip to content

Commit

Permalink
Merge pull request #151 from Regulus0811/feat/live-class
Browse files Browse the repository at this point in the history
ビデオチャットの基本レイアウトの作成
  • Loading branch information
yuminn-k authored Jun 13, 2024
2 parents 618595e + f3345ef commit 6df125f
Show file tree
Hide file tree
Showing 2 changed files with 397 additions and 180 deletions.
284 changes: 193 additions & 91 deletions src/app/classes/[cId]/[mId]/components/manageSubComponents/LiveClass.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
// export default LiveClass;

/* eslint-disable @typescript-eslint/no-explicit-any */
// LiveClass.tsx
import React, {useEffect, useRef, useState} from 'react';

interface LiveClassProps {
Expand All @@ -127,10 +128,10 @@ interface LiveClassProps {
const LiveClass: React.FC<LiveClassProps> = ({classId, userId}) => {
const [classStarted, setClassStarted] = useState(false);
const localVideoRef = useRef<HTMLVideoElement | null>(null);
const peerConnections = useRef<{[key: string]: RTCPeerConnection}>({});
const remoteVideoRefs = useRef<{[key: string]: HTMLVideoElement | null}>({});
const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
const pcRef = useRef<RTCPeerConnection | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const iceCandidatesRef = useRef<{[key: string]: any[]}>({});
const iceCandidatesRef = useRef<any[]>([]);

const startWebSocket = () => {
const ws = new WebSocket(
Expand All @@ -140,33 +141,46 @@ const LiveClass: React.FC<LiveClassProps> = ({classId, userId}) => {

ws.onopen = async () => {
console.log('WebSocket connected');
iceCandidatesRef.current.forEach(candidate => {
ws.send(JSON.stringify({event: 'candidate', data: candidate}));
});
iceCandidatesRef.current = [];

if (pcRef.current) {
try {
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
mediaStream
.getTracks()
.forEach(track => pcRef.current?.addTrack(track, mediaStream));
if (localVideoRef.current) {
localVideoRef.current.srcObject = mediaStream;
}
const offer = await pcRef.current.createOffer();
await pcRef.current.setLocalDescription(offer);
ws.send(
JSON.stringify({
event: 'offer',
data: pcRef.current.localDescription,
})
);
} catch (error) {
console.error('Failed to start media stream', error);
}
}
};

ws.onmessage = async event => {
const {event: evt, data, from} = JSON.parse(event.data);
console.log('Message received:', evt, data, from);

if (evt === 'offer') {
const pc = createPeerConnection(from);
await pc.setRemoteDescription(new RTCSessionDescription(data));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({event: 'answer', data: answer, to: from}));
} else if (evt === 'answer') {
const pc = peerConnections.current[from];
if (pc) {
await pc.setRemoteDescription(new RTCSessionDescription(data));
}
const {event: evt, data} = JSON.parse(event.data);
console.log('Message received:', evt, data);
if (evt === 'answer') {
await pcRef.current?.setRemoteDescription(
new RTCSessionDescription(data)
);
} else if (evt === 'candidate') {
const pc = peerConnections.current[from];
if (pc) {
await pc.addIceCandidate(new RTCIceCandidate(data));
} else {
if (!iceCandidatesRef.current[from]) {
iceCandidatesRef.current[from] = [];
}
iceCandidatesRef.current[from].push(data);
}
await pcRef.current?.addIceCandidate(new RTCIceCandidate(data));
}
};

Expand All @@ -175,91 +189,179 @@ const LiveClass: React.FC<LiveClassProps> = ({classId, userId}) => {
};
};

const createPeerConnection = (peerId: string) => {
const pc = new RTCPeerConnection({
iceServers: [{urls: 'stun:stun.l.google.com:19302'}],
});
peerConnections.current[peerId] = pc;
useEffect(() => {
if (classStarted) {
const pc = new RTCPeerConnection({
iceServers: [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302',
'stun:stun3.l.google.com:19302',
'stun:stun4.l.google.com:19302',
],
},
],
});
pcRef.current = pc;

pc.onicecandidate = event => {
if (event.candidate) {
wsRef.current?.send(
JSON.stringify({
pc.onicecandidate = event => {
if (event.candidate) {
const candidateData = JSON.stringify({
event: 'candidate',
data: event.candidate,
to: peerId,
})
);
}
};

pc.ontrack = event => {
if (!remoteVideoRefs.current[peerId]) {
remoteVideoRefs.current[peerId] = document.createElement('video');
remoteVideoRefs.current[peerId]!.autoplay = true;
remoteVideoRefs.current[peerId]!.playsInline = true;
document
.getElementById('remoteVideos')
?.appendChild(remoteVideoRefs.current[peerId]!);
}
remoteVideoRefs.current[peerId]!.srcObject = event.streams[0];
};

if (iceCandidatesRef.current[peerId]) {
iceCandidatesRef.current[peerId].forEach(candidate => {
pc.addIceCandidate(new RTCIceCandidate(candidate));
});
iceCandidatesRef.current[peerId] = [];
}
data: event.candidate.toJSON(),
});
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(candidateData);
} else {
iceCandidatesRef.current.push(event.candidate.toJSON());
}
}
};

return pc;
};
pc.ontrack = event => {
if (remoteVideoRef.current) {
remoteVideoRef.current.srcObject = event.streams[0];
}
};

useEffect(() => {
if (classStarted) {
startWebSocket();

return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
Object.values(peerConnections.current).forEach(pc => pc.close());
if (wsRef.current) wsRef.current.close();
// eslint-disable-next-line react-hooks/exhaustive-deps
if (localVideoRef.current) localVideoRef.current.srcObject = null;
Object.values(remoteVideoRefs.current).forEach(video => {
if (video) video.srcObject = null;
});
remoteVideoRefs.current = {};
pcRef.current?.close();
wsRef.current?.close();
pcRef.current = null;
wsRef.current = null;
if (localVideoRef.current) {
localVideoRef.current.srcObject = null;
}
if (remoteVideoRef.current) {
remoteVideoRef.current.srcObject = null;
}
};
}
}, [classStarted]);

const handleStartClass = async () => {
const handleStartClass = () => {
setClassStarted(true);
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
if (localVideoRef.current) {
localVideoRef.current.srcObject = mediaStream;
}
wsRef.current?.send(JSON.stringify({event: 'join', data: null}));
};

const handleEndClass = () => {
setClassStarted(false);
};

return (
<div>
{!classStarted ? (
<button onClick={handleStartClass}>Start Class</button>
) : (
<button onClick={handleEndClass}>End Class</button>
)}
<div className="flex flex-col items-center h-screen">
<div className="bg-[#ffffff] border border-gray-400 shadow-md rounded-lg p-6 w-80 flex flex-col items-center mb-8">
{!classStarted ? (
<button
onClick={handleStartClass}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md"
>
수업 시작
</button>
) : (
<button
onClick={handleEndClass}
className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-md"
>
수업 종료
</button>
)}
</div>

{classStarted && (
<div>
<video ref={localVideoRef} autoPlay playsInline muted />
<div id="remoteVideos"></div>
<div
className="flex flex-col items-center border border-gray-400 rounded-lg p-4 mb-8 overflow-y-auto"
style={{
height: '60vh',
}}
>
<div
className="flex flex-col items-center p-4 mb-8"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<video
controls
autoPlay
ref={localVideoRef}
playsInline
style={{width: '80%'}}
/>
<input
type="text"
placeholder="이름 입력"
className="mt-2 px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div
className="flex flex-col items-center p-4 mb-8"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<video
ref={remoteVideoRef}
autoPlay
playsInline
controls
style={{width: '80%'}}
/>
<input
type="text"
placeholder="이름 입력"
className="mt-2 px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div
className="flex flex-col items-center p-4 mb-8"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<video
className="h-full w-full rounded-lg"
controls
autoPlay
playsInline
style={{width: '80%'}}
/>
<input
type="text"
placeholder="이름 입력"
className="mt-2 px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div
className="flex flex-col items-center p-4 mb-8"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<video
className="h-full w-full rounded-lg"
controls
autoPlay
playsInline
style={{width: '80%'}}
/>
<input
type="text"
placeholder="이름 입력"
className="mt-2 px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
)}
</div>
Expand Down
Loading

0 comments on commit 6df125f

Please sign in to comment.