95
95
96
96
// export default LiveClassViewer;
97
97
98
+ /* eslint-disable @typescript-eslint/no-explicit-any */
99
+ // LiveClassViewer.tsx
98
100
import React , { useEffect , useRef , useState } from 'react' ;
99
101
100
102
interface LiveClassViewerProps {
@@ -105,11 +107,10 @@ interface LiveClassViewerProps {
105
107
const LiveClassViewer : React . FC < LiveClassViewerProps > = ( { classId, userId} ) => {
106
108
const [ inClass , setInClass ] = useState ( false ) ;
107
109
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 ) ;
110
112
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 [ ] > ( [ ] ) ;
113
114
114
115
const startWebSocket = ( ) => {
115
116
const ws = new WebSocket (
@@ -119,27 +120,50 @@ const LiveClassViewer: React.FC<LiveClassViewerProps> = ({classId, userId}) => {
119
120
120
121
ws . onopen = async ( ) => {
121
122
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
+ }
122
144
} ;
123
145
124
146
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 ) ;
143
167
}
144
168
}
145
169
} ;
@@ -149,93 +173,184 @@ const LiveClassViewer: React.FC<LiveClassViewerProps> = ({classId, userId}) => {
149
173
} ;
150
174
} ;
151
175
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 ;
157
192
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 ( {
162
196
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
+ } ;
188
206
189
- return pc ;
190
- } ;
207
+ pc . ontrack = event => {
208
+ if ( remoteVideoRef . current ) {
209
+ remoteVideoRef . current . srcObject = event . streams [ 0 ] ;
210
+ }
211
+ } ;
191
212
192
- useEffect ( ( ) => {
193
- if ( inClass ) {
194
213
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
- } ;
205
214
}
215
+
216
+ return ( ) => {
217
+ pcRef . current ?. close ( ) ;
218
+ wsRef . current ?. close ( ) ;
219
+ } ;
206
220
} , [ inClass ] ) ;
207
221
208
- const handleJoinClass = async ( ) => {
222
+ const handleJoinClass = ( ) => {
209
223
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 } ) ) ;
218
224
} ;
219
225
220
226
const handleLeaveClass = ( ) => {
227
+ wsRef . current ?. close ( ) ;
228
+ pcRef . current ?. close ( ) ;
229
+ pcRef . current = null ;
230
+ wsRef . current = null ;
221
231
setInClass ( false ) ;
232
+ if ( localVideoRef . current ) {
233
+ localVideoRef . current . srcObject = null ;
234
+ }
235
+ if ( remoteVideoRef . current ) {
236
+ remoteVideoRef . current . srcObject = null ;
237
+ }
222
238
} ;
223
239
224
240
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
+
231
260
{ 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 >
235
351
</ div >
236
352
) }
237
353
</ div >
238
354
) ;
239
355
} ;
240
-
241
356
export default LiveClassViewer ;
0 commit comments