Skip to content

Commit f3345ef

Browse files
author
Regulus0811
committed
✨ feat: Implement initial layout for the student side in video chat
Created the initial layout for the student side in the video chat functionality. Related issue: #150
1 parent 173e323 commit f3345ef

File tree

1 file changed

+204
-89
lines changed

1 file changed

+204
-89
lines changed

src/app/classes/[cId]/[mId]/components/subComponents/LiveClassViewer.tsx

Lines changed: 204 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595

9696
// export default LiveClassViewer;
9797

98+
/* eslint-disable @typescript-eslint/no-explicit-any */
99+
// LiveClassViewer.tsx
98100
import React, {useEffect, useRef, useState} from 'react';
99101

100102
interface LiveClassViewerProps {
@@ -105,11 +107,10 @@ interface LiveClassViewerProps {
105107
const LiveClassViewer: React.FC<LiveClassViewerProps> = ({classId, userId}) => {
106108
const [inClass, setInClass] = useState(false);
107109
const localVideoRef = useRef<HTMLVideoElement | null>(null);
108-
const peerConnections = useRef<{[key: string]: RTCPeerConnection}>({});
109-
const remoteVideoRefs = useRef<{[key: string]: HTMLVideoElement | null}>({});
110+
const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
111+
const pcRef = useRef<RTCPeerConnection | null>(null);
110112
const wsRef = useRef<WebSocket | null>(null);
111-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
112-
const iceCandidatesRef = useRef<{[key: string]: any[]}>({});
113+
const iceCandidatesRef = useRef<any[]>([]);
113114

114115
const startWebSocket = () => {
115116
const ws = new WebSocket(
@@ -119,27 +120,50 @@ const LiveClassViewer: React.FC<LiveClassViewerProps> = ({classId, userId}) => {
119120

120121
ws.onopen = async () => {
121122
console.log('WebSocket connected');
123+
iceCandidatesRef.current.forEach(candidate => {
124+
ws.send(JSON.stringify({event: 'candidate', data: candidate}));
125+
});
126+
iceCandidatesRef.current = [];
127+
128+
if (pcRef.current) {
129+
try {
130+
const mediaStream = await navigator.mediaDevices.getUserMedia({
131+
video: true,
132+
audio: true,
133+
});
134+
mediaStream
135+
.getTracks()
136+
.forEach(track => pcRef.current?.addTrack(track, mediaStream));
137+
if (localVideoRef.current) {
138+
localVideoRef.current.srcObject = mediaStream;
139+
}
140+
} catch (error) {
141+
console.error('Failed to start media stream', error);
142+
}
143+
}
122144
};
123145

124146
ws.onmessage = async event => {
125-
const {event: evt, data, from} = JSON.parse(event.data);
126-
console.log('Message received:', evt, data, from);
127-
128-
if (evt === 'offer') {
129-
const pc = createPeerConnection(from);
130-
await pc.setRemoteDescription(new RTCSessionDescription(data));
131-
const answer = await pc.createAnswer();
132-
await pc.setLocalDescription(answer);
133-
ws.send(JSON.stringify({event: 'answer', data: answer, to: from}));
134-
} else if (evt === 'candidate') {
135-
const pc = peerConnections.current[from];
136-
if (pc) {
137-
await pc.addIceCandidate(new RTCIceCandidate(data));
138-
} else {
139-
if (!iceCandidatesRef.current[from]) {
140-
iceCandidatesRef.current[from] = [];
141-
}
142-
iceCandidatesRef.current[from].push(data);
147+
const {event: evt, data} = JSON.parse(event.data);
148+
console.log('Message received:', evt, data);
149+
if (evt === 'offer' && pcRef.current) {
150+
try {
151+
console.log('Received offer:', data);
152+
await pcRef.current.setRemoteDescription(
153+
new RTCSessionDescription(data)
154+
);
155+
const answer = await pcRef.current.createAnswer();
156+
await pcRef.current.setLocalDescription(answer);
157+
ws.send(JSON.stringify({event: 'answer', data: answer}));
158+
} catch (error) {
159+
console.error('Failed to handle offer:', error);
160+
}
161+
} else if (evt === 'candidate' && pcRef.current) {
162+
try {
163+
console.log('Received candidate:', data);
164+
await pcRef.current.addIceCandidate(new RTCIceCandidate(data));
165+
} catch (error) {
166+
console.error('Failed to add ICE candidate:', error);
143167
}
144168
}
145169
};
@@ -149,93 +173,184 @@ const LiveClassViewer: React.FC<LiveClassViewerProps> = ({classId, userId}) => {
149173
};
150174
};
151175

152-
const createPeerConnection = (peerId: string) => {
153-
const pc = new RTCPeerConnection({
154-
iceServers: [{urls: 'stun:stun.l.google.com:19302'}],
155-
});
156-
peerConnections.current[peerId] = pc;
176+
useEffect(() => {
177+
if (inClass) {
178+
const pc = new RTCPeerConnection({
179+
iceServers: [
180+
{
181+
urls: [
182+
'stun:stun.l.google.com:19302',
183+
'stun:stun1.l.google.com:19302',
184+
'stun:stun2.l.google.com:19302',
185+
'stun:stun3.l.google.com:19302',
186+
'stun:stun4.l.google.com:19302',
187+
],
188+
},
189+
],
190+
});
191+
pcRef.current = pc;
157192

158-
pc.onicecandidate = event => {
159-
if (event.candidate) {
160-
wsRef.current?.send(
161-
JSON.stringify({
193+
pc.onicecandidate = event => {
194+
if (event.candidate) {
195+
const candidateData = JSON.stringify({
162196
event: 'candidate',
163-
data: event.candidate,
164-
to: peerId,
165-
})
166-
);
167-
}
168-
};
169-
170-
pc.ontrack = event => {
171-
if (!remoteVideoRefs.current[peerId]) {
172-
remoteVideoRefs.current[peerId] = document.createElement('video');
173-
remoteVideoRefs.current[peerId]!.autoplay = true;
174-
remoteVideoRefs.current[peerId]!.playsInline = true;
175-
document
176-
.getElementById('remoteVideos')
177-
?.appendChild(remoteVideoRefs.current[peerId]!);
178-
}
179-
remoteVideoRefs.current[peerId]!.srcObject = event.streams[0];
180-
};
181-
182-
if (iceCandidatesRef.current[peerId]) {
183-
iceCandidatesRef.current[peerId].forEach(candidate => {
184-
pc.addIceCandidate(new RTCIceCandidate(candidate));
185-
});
186-
iceCandidatesRef.current[peerId] = [];
187-
}
197+
data: event.candidate.toJSON(),
198+
});
199+
if (wsRef.current?.readyState === WebSocket.OPEN) {
200+
wsRef.current.send(candidateData);
201+
} else {
202+
iceCandidatesRef.current.push(event.candidate.toJSON());
203+
}
204+
}
205+
};
188206

189-
return pc;
190-
};
207+
pc.ontrack = event => {
208+
if (remoteVideoRef.current) {
209+
remoteVideoRef.current.srcObject = event.streams[0];
210+
}
211+
};
191212

192-
useEffect(() => {
193-
if (inClass) {
194213
startWebSocket();
195-
196-
return () => {
197-
Object.values(peerConnections.current).forEach(pc => pc.close());
198-
if (wsRef.current) wsRef.current.close();
199-
if (localVideoRef.current) localVideoRef.current.srcObject = null;
200-
Object.values(remoteVideoRefs.current).forEach(video => {
201-
if (video) video.srcObject = null;
202-
});
203-
remoteVideoRefs.current = {};
204-
};
205214
}
215+
216+
return () => {
217+
pcRef.current?.close();
218+
wsRef.current?.close();
219+
};
206220
}, [inClass]);
207221

208-
const handleJoinClass = async () => {
222+
const handleJoinClass = () => {
209223
setInClass(true);
210-
const mediaStream = await navigator.mediaDevices.getUserMedia({
211-
video: true,
212-
audio: true,
213-
});
214-
if (localVideoRef.current) {
215-
localVideoRef.current.srcObject = mediaStream;
216-
}
217-
wsRef.current?.send(JSON.stringify({event: 'join', data: null}));
218224
};
219225

220226
const handleLeaveClass = () => {
227+
wsRef.current?.close();
228+
pcRef.current?.close();
229+
pcRef.current = null;
230+
wsRef.current = null;
221231
setInClass(false);
232+
if (localVideoRef.current) {
233+
localVideoRef.current.srcObject = null;
234+
}
235+
if (remoteVideoRef.current) {
236+
remoteVideoRef.current.srcObject = null;
237+
}
222238
};
223239

224240
return (
225-
<div>
226-
{!inClass ? (
227-
<button onClick={handleJoinClass}>Join Class</button>
228-
) : (
229-
<button onClick={handleLeaveClass}>Leave Class</button>
230-
)}
241+
<div className="flex flex-col items-center h-screen">
242+
<div className="bg-[#ffffff] border border-gray-400 shadow-md rounded-lg p-6 w-80 flex flex-col items-center mb-8 mt-12">
243+
{!inClass ? (
244+
<button
245+
onClick={handleJoinClass}
246+
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md"
247+
>
248+
수업 입장
249+
</button>
250+
) : (
251+
<button
252+
onClick={handleLeaveClass}
253+
className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-md"
254+
>
255+
수업 퇴장
256+
</button>
257+
)}
258+
</div>
259+
231260
{inClass && (
232-
<div>
233-
<video ref={localVideoRef} autoPlay playsInline muted />
234-
<div id="remoteVideos"></div>
261+
<div
262+
className="flex flex-col items-center border border-gray-400 rounded-lg p-4 mb-8 overflow-y-auto"
263+
style={{
264+
height: '60vh',
265+
}}
266+
>
267+
<div
268+
className="flex flex-col items-center p-4 mb-8"
269+
style={{
270+
display: 'flex',
271+
flexDirection: 'column',
272+
alignItems: 'center',
273+
}}
274+
>
275+
<video
276+
controls
277+
autoPlay
278+
ref={localVideoRef}
279+
playsInline
280+
style={{width: '80%'}}
281+
/>
282+
<input
283+
type="text"
284+
placeholder="이름 입력"
285+
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"
286+
/>
287+
</div>
288+
<div
289+
className="flex flex-col items-center p-4 mb-8"
290+
style={{
291+
display: 'flex',
292+
flexDirection: 'column',
293+
alignItems: 'center',
294+
}}
295+
>
296+
<video
297+
ref={remoteVideoRef}
298+
autoPlay
299+
playsInline
300+
controls
301+
style={{width: '80%'}}
302+
/>
303+
<input
304+
type="text"
305+
placeholder="이름 입력"
306+
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"
307+
/>
308+
</div>
309+
<div
310+
className="flex flex-col items-center p-4 mb-8"
311+
style={{
312+
display: 'flex',
313+
flexDirection: 'column',
314+
alignItems: 'center',
315+
}}
316+
>
317+
<video
318+
className="h-full w-full rounded-lg"
319+
controls
320+
autoPlay
321+
playsInline
322+
style={{width: '80%'}}
323+
/>
324+
<input
325+
type="text"
326+
placeholder="이름 입력"
327+
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"
328+
/>
329+
</div>
330+
<div
331+
className="flex flex-col items-center p-4 mb-8"
332+
style={{
333+
display: 'flex',
334+
flexDirection: 'column',
335+
alignItems: 'center',
336+
}}
337+
>
338+
<video
339+
className="h-full w-full rounded-lg"
340+
controls
341+
autoPlay
342+
playsInline
343+
style={{width: '80%'}}
344+
/>
345+
<input
346+
type="text"
347+
placeholder="이름 입력"
348+
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"
349+
/>
350+
</div>
235351
</div>
236352
)}
237353
</div>
238354
);
239355
};
240-
241356
export default LiveClassViewer;

0 commit comments

Comments
 (0)