1
1
"use client" ;
2
2
3
- import React , { useEffect , useRef } from "react" ;
3
+ import React , { useEffect , useRef , useState } from "react" ;
4
4
import NextButton from "@/components/common/NextButton" ;
5
5
import { useUserIdStore } from "@/store/useUserIdStore" ;
6
6
import { useStore } from "@/store/useStore" ;
@@ -9,8 +9,17 @@ const RealTimeVideoPage = () => {
9
9
const videoRef = useRef < HTMLVideoElement > ( null ) ;
10
10
const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
11
11
const socketRef = useRef < WebSocket | null > ( null ) ;
12
+ const mediaRecorderRef = useRef < MediaRecorder | null > ( null ) ;
13
+ const recordedChunksRef = useRef < { timestamp : number ; data : Blob } [ ] > ( [ ] ) ; // 청크와 타임스탬프 저장
14
+ const captureIntervalRef = useRef < number | null > ( null ) ;
15
+
16
+ const CHUNK_SIZE = 1000 ; // 1초마다 녹화 데이터를 저장
17
+ const BUFFER_DURATION = 20 * 1000 ; // 20초 간의 데이터를 저장
18
+
19
+ const [ isProcessing , setIsProcessing ] = useState ( false ) ; // 부정행위 감지 처리 중 여부 상태
12
20
13
21
const userId = useStore ( useUserIdStore , ( state ) => state . userId ) ;
22
+ const examId = 1 ;
14
23
const setupWebSocket = ( ) => {
15
24
console . log ( process . env . NEXT_PUBLIC_WEBSOCKET_KEY ) ;
16
25
console . log ( userId ) ;
@@ -21,11 +30,11 @@ const RealTimeVideoPage = () => {
21
30
}
22
31
23
32
const socket = new WebSocket (
24
- `${ process . env . NEXT_PUBLIC_WEBSOCKET_KEY } /${ userId } `
33
+ `${ process . env . NEXT_PUBLIC_WEBSOCKET_KEY } /${ userId } / ${ examId } `
25
34
) ;
26
35
27
36
socket . onopen = ( ) => {
28
- console . log ( `WebSocket 연결 성공: ${ userId } ` ) ;
37
+ console . log ( `WebSocket 연결 성공: ${ userId } , ${ examId } ` ) ;
29
38
} ;
30
39
31
40
socket . onerror = ( error ) => {
@@ -37,25 +46,143 @@ const RealTimeVideoPage = () => {
37
46
setTimeout ( setupWebSocket , 3000 ) ; // 3초 후 재시도
38
47
} ;
39
48
49
+ // 부정행위 감지 메시지 처리
50
+ socket . onmessage = ( event ) => {
51
+ const message = JSON . parse ( event . data ) ;
52
+ console . log ( "WebSocket 메시지 수신:" , message ) ;
53
+
54
+ // 부정행위 감지 메시지가 있을 경우
55
+ if ( message ) {
56
+ console . log ( "부정행위 감지:" , message . timestamp ) ;
57
+
58
+ // 부정행위 비디오 저장 호출
59
+ sendCheatingVideo ( message . timestamp ) ;
60
+ }
61
+ } ;
62
+
40
63
socketRef . current = socket ;
41
64
} ;
42
65
43
- // 비디오 스트림 가져오기
44
66
const startStreaming = async ( ) => {
45
67
try {
46
68
const constraints = { video : true } ;
47
69
const stream = await navigator . mediaDevices . getUserMedia ( constraints ) ;
70
+
48
71
if ( videoRef . current ) {
49
72
videoRef . current . srcObject = stream ;
50
73
console . log ( "비디오 스트림 시작" ) ;
51
74
}
75
+
76
+ startRecording ( stream ) ; // 비디오 스트림과 함께 녹화 시작
52
77
} catch ( error ) {
53
78
console . error ( "비디오 스트림 가져오기 오류:" , error ) ;
54
79
alert ( "카메라 권한을 허용해주세요." ) ;
55
80
}
56
81
} ;
57
82
58
83
// Canvas를 사용해 비디오 프레임을 WebSocket으로 전송
84
+ const startRecording = ( stream : MediaStream ) => {
85
+ mediaRecorderRef . current = new MediaRecorder ( stream , {
86
+ mimeType : "video/webm; codecs=vp8" ,
87
+ } ) ;
88
+
89
+ mediaRecorderRef . current . ondataavailable = ( event ) => {
90
+ if ( event . data . size > 0 ) {
91
+ const timestamp = Date . now ( ) ;
92
+ recordedChunksRef . current . push ( {
93
+ timestamp : timestamp ,
94
+ data : event . data ,
95
+ } ) ;
96
+
97
+ // 슬라이딩 윈도우 방식으로 오래된 데이터 삭제
98
+ const currentTime = Date . now ( ) ;
99
+ recordedChunksRef . current = recordedChunksRef . current . filter (
100
+ ( chunk ) => chunk . timestamp >= currentTime - BUFFER_DURATION
101
+ ) ;
102
+ }
103
+ } ;
104
+
105
+ mediaRecorderRef . current . onerror = ( e ) => {
106
+ console . error ( "MediaRecorder 오류 발생:" , e ) ;
107
+ } ;
108
+
109
+ mediaRecorderRef . current . start ( CHUNK_SIZE ) ; // **녹화 시작**
110
+ console . log ( "MediaRecorder 시작" ) ;
111
+ } ;
112
+
113
+ const stopRecording = ( ) => {
114
+ if ( mediaRecorderRef . current ) {
115
+ mediaRecorderRef . current . stop ( ) ;
116
+ console . log ( "MediaRecorder 중지" ) ;
117
+ }
118
+ } ;
119
+
120
+ // 부정행위 비디오 처리 및 저장
121
+ const sendCheatingVideo = async (
122
+ cheatingTimestamp : string | number | Date
123
+ ) => {
124
+ try {
125
+ setIsProcessing ( true ) ; // 부정행위 감지 시작
126
+
127
+ console . log ( "부정행위 발생 타임스탬프:" , cheatingTimestamp ) ;
128
+
129
+ const cheatingDate = new Date ( cheatingTimestamp ) ;
130
+ if ( isNaN ( cheatingDate . getTime ( ) ) ) {
131
+ throw new Error ( "Invalid cheatingTimestamp: " + cheatingTimestamp ) ;
132
+ }
133
+
134
+ const cheatingTime = cheatingDate . getTime ( ) ;
135
+ const startTime = cheatingTime - 5000 ; // 부정행위 5초 전
136
+ const endTime = cheatingTime + 5000 ; // 부정행위 5초 후
137
+
138
+ console . log ( "탐지 이후 5초 데이터를 수집 중..." ) ;
139
+ setTimeout ( ( ) => {
140
+ mediaRecorderRef . current ?. stop ( ) ;
141
+ } , 5000 ) ;
142
+
143
+ // MediaRecorder가 멈춘 후 데이터를 처리
144
+ mediaRecorderRef . current ! . onstop = ( ) => {
145
+ const previousChunks = recordedChunksRef . current . filter (
146
+ ( chunk ) =>
147
+ chunk . timestamp >= startTime && chunk . timestamp <= cheatingTime
148
+ ) ;
149
+ console . log ( `탐지 이전 데이터: ${ previousChunks . length } 청크` ) ;
150
+
151
+ const postCheatingChunks = recordedChunksRef . current . filter (
152
+ ( chunk ) =>
153
+ chunk . timestamp > cheatingTime && chunk . timestamp <= endTime
154
+ ) ;
155
+ console . log ( `탐지 이후 데이터: ${ postCheatingChunks . length } 청크` ) ;
156
+
157
+ const allChunks = [ ...previousChunks , ...postCheatingChunks ] ;
158
+ const blob = new Blob (
159
+ allChunks . map ( ( chunk ) => chunk . data ) ,
160
+ {
161
+ type : "video/webm" ,
162
+ }
163
+ ) ;
164
+
165
+ console . log ( `최종 Blob 크기: ${ blob . size / 1024 } KB` ) ;
166
+
167
+ const url = URL . createObjectURL ( blob ) ;
168
+ const a = document . createElement ( "a" ) ;
169
+ a . href = url ;
170
+ a . download = `cheating_${ new Date ( ) . toISOString ( ) } .webm` ;
171
+ a . click ( ) ;
172
+ URL . revokeObjectURL ( url ) ;
173
+
174
+ console . log ( "부정행위 비디오 저장 완료" ) ;
175
+ // 이 부분은 녹화가 중지된 후, 데이터가 완전히 처리된 후에 실행되어야 합니다.
176
+ startRecording ( videoRef . current ?. srcObject as MediaStream ) ;
177
+ } ;
178
+ } catch ( error ) {
179
+ console . error ( "부정행위 이벤트 처리 중 오류:" , error ) ;
180
+ } finally {
181
+ setIsProcessing ( false ) ; // 부정행위 처리 끝
182
+ console . log ( isProcessing ) ;
183
+ }
184
+ } ;
185
+
59
186
const captureAndSendFrame = ( ) => {
60
187
if (
61
188
canvasRef . current &&
@@ -65,40 +192,36 @@ const RealTimeVideoPage = () => {
65
192
const canvas = canvasRef . current ;
66
193
const video = videoRef . current ;
67
194
68
- // Canvas 크기를 비디오와 동일하게 설정
69
195
canvas . width = video . videoWidth ;
70
196
canvas . height = video . videoHeight ;
71
197
72
- // Canvas에 현재 비디오 프레임 그리기
73
198
const context = canvas . getContext ( "2d" ) ;
74
199
if ( context ) {
75
200
context . drawImage ( video , 0 , 0 , canvas . width , canvas . height ) ;
76
201
77
- // 캡처된 Canvas를 Base64로 변환
78
- const base64Data = canvas . toDataURL ( "image/jpeg" , 0.7 ) ; // 품질 70%
79
- const base64String = base64Data . split ( "," ) [ 1 ] ; // "data:image/jpeg;base64," 부분 제거
202
+ const base64Data = canvas . toDataURL ( "image/jpeg" , 0.3 ) ;
203
+ const base64String = base64Data . split ( "," ) [ 1 ] ;
80
204
81
- if ( socketRef . current ) {
82
- socketRef . current . send ( base64String ) ;
83
- console . log ( "Base64 이미지 전송" ) ;
84
- }
205
+ socketRef . current . send ( base64String ) ;
206
+ console . log ( "WebSocket으로 프레임 전송" ) ;
85
207
}
86
208
}
87
209
} ;
88
210
89
- // 초기화 작업: WebSocket 연결, 비디오 스트리밍 시작, 시작 API 호출
90
211
useEffect ( ( ) => {
91
212
const initialize = async ( ) => {
92
213
setupWebSocket ( ) ;
93
214
await startStreaming ( ) ;
94
215
95
- // 0.5초에 한 번씩 프레임 캡처 및 전송
96
- const captureInterval = setInterval ( captureAndSendFrame , 500 ) ;
216
+ // 0.5초에 한 번씩 프레임 캡처 및 전송
217
+ captureIntervalRef . current = window . setInterval ( captureAndSendFrame , 500 ) ;
97
218
98
219
return ( ) => {
99
- clearInterval ( captureInterval ) ;
220
+ if ( captureIntervalRef . current ) {
221
+ clearInterval ( captureIntervalRef . current ) ;
222
+ }
100
223
101
- // WebSocket 연결 종료
224
+ stopRecording ( ) ;
102
225
if ( socketRef . current ) {
103
226
socketRef . current . close ( ) ;
104
227
}
@@ -115,7 +238,7 @@ const RealTimeVideoPage = () => {
115
238
autoPlay
116
239
playsInline
117
240
className = "w-screen h-screen object-cover border border-gray-300"
118
- style = { { transform : "scaleX(-1)" } } // 좌우 반전
241
+ style = { { transform : "scaleX(-1)" } }
119
242
/>
120
243
< canvas ref = { canvasRef } style = { { display : "none" } } />
121
244
< div className = "fixed bottom-6 right-6" >
0 commit comments