117
117
// export default LiveClass;
118
118
119
119
/* eslint-disable @typescript-eslint/no-explicit-any */
120
+ // LiveClass.tsx
120
121
import React , { useEffect , useRef , useState } from 'react' ;
121
122
122
123
interface LiveClassProps {
@@ -127,10 +128,10 @@ interface LiveClassProps {
127
128
const LiveClass : React . FC < LiveClassProps > = ( { classId, userId} ) => {
128
129
const [ classStarted , setClassStarted ] = useState ( false ) ;
129
130
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 ) ;
132
133
const wsRef = useRef < WebSocket | null > ( null ) ;
133
- const iceCandidatesRef = useRef < { [ key : string ] : any [ ] } > ( { } ) ;
134
+ const iceCandidatesRef = useRef < any [ ] > ( [ ] ) ;
134
135
135
136
const startWebSocket = ( ) => {
136
137
const ws = new WebSocket (
@@ -140,33 +141,46 @@ const LiveClass: React.FC<LiveClassProps> = ({classId, userId}) => {
140
141
141
142
ws . onopen = async ( ) => {
142
143
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
+ }
143
173
} ;
144
174
145
175
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
+ ) ;
160
182
} 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 ) ) ;
170
184
}
171
185
} ;
172
186
@@ -175,91 +189,179 @@ const LiveClass: React.FC<LiveClassProps> = ({classId, userId}) => {
175
189
} ;
176
190
} ;
177
191
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 ;
183
208
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 ( {
188
212
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
+ } ;
214
222
215
- return pc ;
216
- } ;
223
+ pc . ontrack = event => {
224
+ if ( remoteVideoRef . current ) {
225
+ remoteVideoRef . current . srcObject = event . streams [ 0 ] ;
226
+ }
227
+ } ;
217
228
218
- useEffect ( ( ) => {
219
- if ( classStarted ) {
220
229
startWebSocket ( ) ;
221
230
222
231
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
+ }
232
242
} ;
233
243
}
234
244
} , [ classStarted ] ) ;
235
245
236
- const handleStartClass = async ( ) => {
246
+ const handleStartClass = ( ) => {
237
247
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 } ) ) ;
246
248
} ;
247
249
248
250
const handleEndClass = ( ) => {
249
251
setClassStarted ( false ) ;
250
252
} ;
251
253
252
254
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
+
259
274
{ 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 >
263
365
</ div >
264
366
) }
265
367
</ div >
0 commit comments