Skip to content

Commit 6df125f

Browse files
authored
Merge pull request #151 from Regulus0811/feat/live-class
ビデオチャットの基本レイアウトの作成
2 parents 618595e + f3345ef commit 6df125f

File tree

2 files changed

+397
-180
lines changed

2 files changed

+397
-180
lines changed

src/app/classes/[cId]/[mId]/components/manageSubComponents/LiveClass.tsx

Lines changed: 193 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
// export default LiveClass;
118118

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

122123
interface LiveClassProps {
@@ -127,10 +128,10 @@ interface LiveClassProps {
127128
const LiveClass: React.FC<LiveClassProps> = ({classId, userId}) => {
128129
const [classStarted, setClassStarted] = useState(false);
129130
const localVideoRef = useRef<HTMLVideoElement | null>(null);
130-
const peerConnections = useRef<{[key: string]: RTCPeerConnection}>({});
131-
const remoteVideoRefs = useRef<{[key: string]: HTMLVideoElement | null}>({});
131+
const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
132+
const pcRef = useRef<RTCPeerConnection | null>(null);
132133
const wsRef = useRef<WebSocket | null>(null);
133-
const iceCandidatesRef = useRef<{[key: string]: any[]}>({});
134+
const iceCandidatesRef = useRef<any[]>([]);
134135

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

141142
ws.onopen = async () => {
142143
console.log('WebSocket connected');
144+
iceCandidatesRef.current.forEach(candidate => {
145+
ws.send(JSON.stringify({event: 'candidate', data: candidate}));
146+
});
147+
iceCandidatesRef.current = [];
148+
149+
if (pcRef.current) {
150+
try {
151+
const mediaStream = await navigator.mediaDevices.getUserMedia({
152+
video: true,
153+
audio: true,
154+
});
155+
mediaStream
156+
.getTracks()
157+
.forEach(track => pcRef.current?.addTrack(track, mediaStream));
158+
if (localVideoRef.current) {
159+
localVideoRef.current.srcObject = mediaStream;
160+
}
161+
const offer = await pcRef.current.createOffer();
162+
await pcRef.current.setLocalDescription(offer);
163+
ws.send(
164+
JSON.stringify({
165+
event: 'offer',
166+
data: pcRef.current.localDescription,
167+
})
168+
);
169+
} catch (error) {
170+
console.error('Failed to start media stream', error);
171+
}
172+
}
143173
};
144174

145175
ws.onmessage = async event => {
146-
const {event: evt, data, from} = JSON.parse(event.data);
147-
console.log('Message received:', evt, data, from);
148-
149-
if (evt === 'offer') {
150-
const pc = createPeerConnection(from);
151-
await pc.setRemoteDescription(new RTCSessionDescription(data));
152-
const answer = await pc.createAnswer();
153-
await pc.setLocalDescription(answer);
154-
ws.send(JSON.stringify({event: 'answer', data: answer, to: from}));
155-
} else if (evt === 'answer') {
156-
const pc = peerConnections.current[from];
157-
if (pc) {
158-
await pc.setRemoteDescription(new RTCSessionDescription(data));
159-
}
176+
const {event: evt, data} = JSON.parse(event.data);
177+
console.log('Message received:', evt, data);
178+
if (evt === 'answer') {
179+
await pcRef.current?.setRemoteDescription(
180+
new RTCSessionDescription(data)
181+
);
160182
} else if (evt === 'candidate') {
161-
const pc = peerConnections.current[from];
162-
if (pc) {
163-
await pc.addIceCandidate(new RTCIceCandidate(data));
164-
} else {
165-
if (!iceCandidatesRef.current[from]) {
166-
iceCandidatesRef.current[from] = [];
167-
}
168-
iceCandidatesRef.current[from].push(data);
169-
}
183+
await pcRef.current?.addIceCandidate(new RTCIceCandidate(data));
170184
}
171185
};
172186

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

178-
const createPeerConnection = (peerId: string) => {
179-
const pc = new RTCPeerConnection({
180-
iceServers: [{urls: 'stun:stun.l.google.com:19302'}],
181-
});
182-
peerConnections.current[peerId] = pc;
192+
useEffect(() => {
193+
if (classStarted) {
194+
const pc = new RTCPeerConnection({
195+
iceServers: [
196+
{
197+
urls: [
198+
'stun:stun.l.google.com:19302',
199+
'stun:stun1.l.google.com:19302',
200+
'stun:stun2.l.google.com:19302',
201+
'stun:stun3.l.google.com:19302',
202+
'stun:stun4.l.google.com:19302',
203+
],
204+
},
205+
],
206+
});
207+
pcRef.current = pc;
183208

184-
pc.onicecandidate = event => {
185-
if (event.candidate) {
186-
wsRef.current?.send(
187-
JSON.stringify({
209+
pc.onicecandidate = event => {
210+
if (event.candidate) {
211+
const candidateData = JSON.stringify({
188212
event: 'candidate',
189-
data: event.candidate,
190-
to: peerId,
191-
})
192-
);
193-
}
194-
};
195-
196-
pc.ontrack = event => {
197-
if (!remoteVideoRefs.current[peerId]) {
198-
remoteVideoRefs.current[peerId] = document.createElement('video');
199-
remoteVideoRefs.current[peerId]!.autoplay = true;
200-
remoteVideoRefs.current[peerId]!.playsInline = true;
201-
document
202-
.getElementById('remoteVideos')
203-
?.appendChild(remoteVideoRefs.current[peerId]!);
204-
}
205-
remoteVideoRefs.current[peerId]!.srcObject = event.streams[0];
206-
};
207-
208-
if (iceCandidatesRef.current[peerId]) {
209-
iceCandidatesRef.current[peerId].forEach(candidate => {
210-
pc.addIceCandidate(new RTCIceCandidate(candidate));
211-
});
212-
iceCandidatesRef.current[peerId] = [];
213-
}
213+
data: event.candidate.toJSON(),
214+
});
215+
if (wsRef.current?.readyState === WebSocket.OPEN) {
216+
wsRef.current.send(candidateData);
217+
} else {
218+
iceCandidatesRef.current.push(event.candidate.toJSON());
219+
}
220+
}
221+
};
214222

215-
return pc;
216-
};
223+
pc.ontrack = event => {
224+
if (remoteVideoRef.current) {
225+
remoteVideoRef.current.srcObject = event.streams[0];
226+
}
227+
};
217228

218-
useEffect(() => {
219-
if (classStarted) {
220229
startWebSocket();
221230

222231
return () => {
223-
// eslint-disable-next-line react-hooks/exhaustive-deps
224-
Object.values(peerConnections.current).forEach(pc => pc.close());
225-
if (wsRef.current) wsRef.current.close();
226-
// eslint-disable-next-line react-hooks/exhaustive-deps
227-
if (localVideoRef.current) localVideoRef.current.srcObject = null;
228-
Object.values(remoteVideoRefs.current).forEach(video => {
229-
if (video) video.srcObject = null;
230-
});
231-
remoteVideoRefs.current = {};
232+
pcRef.current?.close();
233+
wsRef.current?.close();
234+
pcRef.current = null;
235+
wsRef.current = null;
236+
if (localVideoRef.current) {
237+
localVideoRef.current.srcObject = null;
238+
}
239+
if (remoteVideoRef.current) {
240+
remoteVideoRef.current.srcObject = null;
241+
}
232242
};
233243
}
234244
}, [classStarted]);
235245

236-
const handleStartClass = async () => {
246+
const handleStartClass = () => {
237247
setClassStarted(true);
238-
const mediaStream = await navigator.mediaDevices.getUserMedia({
239-
video: true,
240-
audio: true,
241-
});
242-
if (localVideoRef.current) {
243-
localVideoRef.current.srcObject = mediaStream;
244-
}
245-
wsRef.current?.send(JSON.stringify({event: 'join', data: null}));
246248
};
247249

248250
const handleEndClass = () => {
249251
setClassStarted(false);
250252
};
251253

252254
return (
253-
<div>
254-
{!classStarted ? (
255-
<button onClick={handleStartClass}>Start Class</button>
256-
) : (
257-
<button onClick={handleEndClass}>End Class</button>
258-
)}
255+
<div className="flex flex-col items-center h-screen">
256+
<div className="bg-[#ffffff] border border-gray-400 shadow-md rounded-lg p-6 w-80 flex flex-col items-center mb-8">
257+
{!classStarted ? (
258+
<button
259+
onClick={handleStartClass}
260+
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md"
261+
>
262+
수업 시작
263+
</button>
264+
) : (
265+
<button
266+
onClick={handleEndClass}
267+
className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-md"
268+
>
269+
수업 종료
270+
</button>
271+
)}
272+
</div>
273+
259274
{classStarted && (
260-
<div>
261-
<video ref={localVideoRef} autoPlay playsInline muted />
262-
<div id="remoteVideos"></div>
275+
<div
276+
className="flex flex-col items-center border border-gray-400 rounded-lg p-4 mb-8 overflow-y-auto"
277+
style={{
278+
height: '60vh',
279+
}}
280+
>
281+
<div
282+
className="flex flex-col items-center p-4 mb-8"
283+
style={{
284+
display: 'flex',
285+
flexDirection: 'column',
286+
alignItems: 'center',
287+
}}
288+
>
289+
<video
290+
controls
291+
autoPlay
292+
ref={localVideoRef}
293+
playsInline
294+
style={{width: '80%'}}
295+
/>
296+
<input
297+
type="text"
298+
placeholder="이름 입력"
299+
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"
300+
/>
301+
</div>
302+
<div
303+
className="flex flex-col items-center p-4 mb-8"
304+
style={{
305+
display: 'flex',
306+
flexDirection: 'column',
307+
alignItems: 'center',
308+
}}
309+
>
310+
<video
311+
ref={remoteVideoRef}
312+
autoPlay
313+
playsInline
314+
controls
315+
style={{width: '80%'}}
316+
/>
317+
<input
318+
type="text"
319+
placeholder="이름 입력"
320+
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"
321+
/>
322+
</div>
323+
<div
324+
className="flex flex-col items-center p-4 mb-8"
325+
style={{
326+
display: 'flex',
327+
flexDirection: 'column',
328+
alignItems: 'center',
329+
}}
330+
>
331+
<video
332+
className="h-full w-full rounded-lg"
333+
controls
334+
autoPlay
335+
playsInline
336+
style={{width: '80%'}}
337+
/>
338+
<input
339+
type="text"
340+
placeholder="이름 입력"
341+
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"
342+
/>
343+
</div>
344+
<div
345+
className="flex flex-col items-center p-4 mb-8"
346+
style={{
347+
display: 'flex',
348+
flexDirection: 'column',
349+
alignItems: 'center',
350+
}}
351+
>
352+
<video
353+
className="h-full w-full rounded-lg"
354+
controls
355+
autoPlay
356+
playsInline
357+
style={{width: '80%'}}
358+
/>
359+
<input
360+
type="text"
361+
placeholder="이름 입력"
362+
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"
363+
/>
364+
</div>
263365
</div>
264366
)}
265367
</div>

0 commit comments

Comments
 (0)