Skip to content

Commit

Permalink
FE: [feat] 부정행위 탐지 영상 저장 #53
Browse files Browse the repository at this point in the history
  • Loading branch information
hyeona01 committed Dec 8, 2024
1 parent d2b4747 commit aecb6eb
Showing 1 changed file with 62 additions and 127 deletions.
189 changes: 62 additions & 127 deletions src/frontend/eyesee-user/src/app/exam-room/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,20 @@ import { useStore } from "@/store/useStore";
const RealTimeVideoPage = () => {
// 수험자 실시간 화면이 담기는 공간
const videoRef = useRef<HTMLVideoElement>(null);

// 캠버스에서 프레임을 캡쳐
const canvasRef = useRef<HTMLCanvasElement>(null);

// 웹소켓 연결 관리
const socketRef = useRef<WebSocket | null>(null);

// 실시간 비디오 녹화
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
// 미디어 스트림을 저장
const streamRef = useRef<MediaStream | null>(null);

// 녹화된 비디오 데이터를 청크 단위로 저장
const recordedChunksRef = useRef<{ timestamp: number; data: Blob }[]>([]); // 청크와 타임스탬프 저장

const recordedChunksRef = useRef<Blob[]>([]);
const captureIntervalRef = useRef<number | null>(null);

const CHUNK_SIZE = 1000; // 1초마다 녹화 데이터를 저장
const BUFFER_DURATION = 20 * 1000; // 20초 간의 데이터를 저장

// 부정행위 감지 처리 중 여부
const [isProcessing, setIsProcessing] = useState(false);

const userId = useStore(useUserStore, (state) => state.userId);
Expand All @@ -35,7 +30,6 @@ const RealTimeVideoPage = () => {
const setupWebSocket = () => {
console.log(process.env.NEXT_PUBLIC_WEBSOCKET_KEY);
console.log(userId, examId);

if (!userId) {
console.log("userId가 설정되지 않았습니다.");
return;
Expand All @@ -55,7 +49,7 @@ const RealTimeVideoPage = () => {

socket.onclose = (event) => {
console.log("WebSocket 연결 종료. 재연결 시도 중", event.reason);
setTimeout(setupWebSocket, 3000); // 3초 후 재시도
setTimeout(setupWebSocket, 3000);
};

// 부정행위 감지 메시지 처리
Expand All @@ -66,9 +60,7 @@ const RealTimeVideoPage = () => {
// 부정행위 감지 메시지가 있을 경우
if (message) {
console.log("부정행위 감지:", message.timestamp);

// 부정행위 비디오 저장 호출
sendCheatingVideo(message.timestamp);
startRecordingForCheating(); // 부정행위 비디오 저장 함수 호출
}
};

Expand All @@ -77,152 +69,83 @@ const RealTimeVideoPage = () => {

const startStreaming = async () => {
try {
const constraints = { video: true };
const constraints = { video: true, audio: false };
const stream = await navigator.mediaDevices.getUserMedia(constraints);
streamRef.current = stream;

if (videoRef.current) {
videoRef.current.srcObject = stream;
console.log("비디오 스트림 시작");
}

startRecording(stream); // 비디오 스트림과 함께 녹화 시작
return stream;
} catch (error) {
console.error("비디오 스트림 가져오기 오류:", error);
alert("카메라 권한을 허용해주세요.");
throw error;
}
};

// Canvas를 사용해 비디오 프레임을 WebSocket으로 전송
const startRecording = (stream: MediaStream) => {
recordedChunksRef.current = []; // 기존 청크 초기화

mediaRecorderRef.current = new MediaRecorder(stream, {
mimeType: "video/webm; codecs=vp9",
mimeType: "video/webm",
});

mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data.size > 0) {
const timestamp = Date.now();
recordedChunksRef.current.push(event.data);
}
};

// 새로 들어온 데이터 추가
recordedChunksRef.current.push({
timestamp: timestamp,
data: event.data,
mediaRecorderRef.current.onstop = () => {
if (recordedChunksRef.current.length > 0) {
const blob = new Blob(recordedChunksRef.current, {
type: "video/webm",
});
const url = URL.createObjectURL(blob);

// if (isProcessing) return; // 부정행위 비디오 처리 및 저장 중에는 비디오 프레임 업데이트 정지
// 슬라이딩 윈도우 방식으로 오래된 데이터 삭제
recordedChunksRef.current = recordedChunksRef.current.filter(
(chunk) => chunk.timestamp >= timestamp - BUFFER_DURATION
);
const a = document.createElement("a");
a.href = url;
a.download = `cheating_${new Date().toISOString()}.webm`;
a.click();

console.log(
`현재 청크 수: ${recordedChunksRef.current.length}, 유지 시간: ${BUFFER_DURATION}ms`
);
console.log(`비디오 크기: ${blob.size / 1024} KB`);
console.log("부정행위 비디오 저장 완료");
}
};

mediaRecorderRef.current.onerror = (e) => {
console.error("MediaRecorder 오류 발생:", e);
};

mediaRecorderRef.current.start(CHUNK_SIZE); // **녹화 시작**
console.log("MediaRecorder 시작");
mediaRecorderRef.current.start();
};

const stopRecording = () => {
if (mediaRecorderRef.current) {
mediaRecorderRef.current.stop();
console.log("MediaRecorder 중지");
}
};

// ===== AI단에서 웹소켓 메시지를 수신하면 부정행위 비디오 처리 및 저장 =====
const sendCheatingVideo = async (cheatingTimestamp: string) => {
const startRecordingForCheating = async () => {
try {
setIsProcessing(true); // 부정행위 감지 시작

console.log("부정행위 발생 타임스탬프:", cheatingTimestamp);
setIsProcessing(true);

const cheatingDate = new Date(cheatingTimestamp);
console.log("cheatingDate", cheatingDate);

if (isNaN(cheatingDate.getTime())) {
throw new Error("Invalid cheatingTimestamp: " + cheatingTimestamp);
// 이미 녹화 중이면 기존 녹화 중지
if (mediaRecorderRef.current) {
mediaRecorderRef.current.stop();
}

const cheatingTime = cheatingDate.getTime();
const startTime = cheatingTime - 5000; // 부정행위 5초 전
const endTime = cheatingTime + 5000; // 부정행위 5초 후
// 스트림이 없으면 새로 생성
const stream = streamRef.current || (await startStreaming());

// 5초간 녹화
startRecording(stream);

console.log("탐지 이후 5초 데이터를 수집 중...");
// 5초 후 녹화 중지
setTimeout(() => {
mediaRecorderRef.current?.stop();
if (mediaRecorderRef.current) {
mediaRecorderRef.current.stop();
}
setIsProcessing(false);
}, 5000);

// MediaRecorder가 멈춘 후 데이터를 처리
mediaRecorderRef.current!.onstop = () => {
console.log("recordedChunksRef.current :", recordedChunksRef.current);

const previousChunks = recordedChunksRef.current.filter(
(chunk) =>
chunk.timestamp >= startTime && chunk.timestamp <= cheatingTime
);
console.log(`탐지 이전 데이터: ${previousChunks.length} 청크`);
console.log(`탐지 이전 데이터: ${previousChunks}`);

const postCheatingChunks = recordedChunksRef.current.filter(
(chunk) =>
chunk.timestamp > cheatingTime && chunk.timestamp <= endTime
);
console.log(`탐지 이후 데이터: ${postCheatingChunks.length} 청크`);
console.log(`탐지 이후 데이터: ${postCheatingChunks}`);

const allChunks = [...previousChunks, ...postCheatingChunks];
console.log(`allChunks: ${allChunks.length} 청크`);
console.log(`allChunks: ${allChunks}`);
allChunks.forEach((chunk, index) => {
console.log(`손실 탐지 Chunk ${index}:`, chunk.timestamp, chunk.data);
});

const blob = new Blob(
allChunks.map((chunk) => chunk.data),
{
type: "video/webm; codecs=vp9",
}
);
// 디버깅
console.log("blob: ", blob);
console.log(`최종 Blob 크기: ${blob.size / 1024} KB`);
// --

const url = URL.createObjectURL(blob);
// 디버깅
console.log("Generated Blob URL:", url);

// 비디오 태그 생성으로 디버깅
const video = document.createElement("video");
video.src = url;
video.controls = true;
document.body.appendChild(video);
// --

const a = document.createElement("a");
a.href = url;
a.download = `cheating_${new Date().toISOString()}.webm`;
a.click();

console.log("부정행위 비디오 저장 완료");
// 이 부분은 녹화가 중지된 후, 데이터가 완전히 처리된 후에 실행되어야 합니다.
startRecording(videoRef.current?.srcObject as MediaStream);
};
} catch (error) {
console.error("부정행위 이벤트 처리 중 오류:", error);
} finally {
setIsProcessing(false); // 부정행위 처리 끝
console.log(isProcessing);
console.error("부정행위 이벤트 처리 중 오류", error);
setIsProcessing(false);
}
};

// AI 단으로 실시간 영상 송신
const captureAndSendFrame = () => {
if (
canvasRef.current &&
Expand All @@ -243,28 +166,40 @@ const RealTimeVideoPage = () => {
const base64String = base64Data.split(",")[1];

socketRef.current.send(base64String);
console.log("WebSocket으로 프레임 전송");
}
}
};

useEffect(() => {
const initialize = async () => {
setupWebSocket();
await startStreaming();
try {
setupWebSocket();
await startStreaming();

// 0.5초에 한 번씩 프레임 캡처 및 전송
captureIntervalRef.current = window.setInterval(captureAndSendFrame, 500);
captureIntervalRef.current = window.setInterval(
captureAndSendFrame,
500
);
} catch (error) {
console.error("초기화 중 오류:", error);
}

return () => {
if (captureIntervalRef.current) {
clearInterval(captureIntervalRef.current);
}

stopRecording();
if (mediaRecorderRef.current) {
mediaRecorderRef.current.stop();
}

if (socketRef.current) {
socketRef.current.close();
}

if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop());
}
};
};

Expand Down

0 comments on commit aecb6eb

Please sign in to comment.