From 715de35c222a0eb63e751411b19a412b12347bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B1=EC=9D=B8?= Date: Sat, 11 Nov 2023 19:33:04 +0900 Subject: [PATCH] =?UTF-8?q?[NDD-87]=20Interview=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20camera=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=B6=80=EC=97=AC=20(7h/8h)=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: video UI 기능 구현 * fix: log 문제 * fix: InterviewCamera 파일시스템 문제 해결 * chore: 상대경로 수정 * chore: CameraRef 이름 변경 --- .../interviewPage/InterviewCamera.tsx | 134 +++++++++++++++++- 1 file changed, 129 insertions(+), 5 deletions(-) diff --git a/FE/src/components/interviewPage/InterviewCamera.tsx b/FE/src/components/interviewPage/InterviewCamera.tsx index 0e1466f..55c188f 100644 --- a/FE/src/components/interviewPage/InterviewCamera.tsx +++ b/FE/src/components/interviewPage/InterviewCamera.tsx @@ -1,19 +1,143 @@ +import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; import { css } from '@emotion/react'; const InterviewCamera: React.FC = () => { + const [stream, setStream] = useState(null); + const [recording, setRecording] = useState(false); + const [recordedBlobs, setRecordedBlobs] = useState([]); + const [selectedMimeType, setSelectedMimeType] = useState(''); + + const mirrorVideoRef = useRef(null); + const mediaRecorderRef = useRef(null); + + useLayoutEffect(() => { + const mimeTypes = getSupportedMimeTypes(); + if (mimeTypes.length > 0) { + setSelectedMimeType(mimeTypes[0]); + } + }, []); + + useEffect(() => { + if (!stream) { + void getMedia(); + } + + return () => { + if (stream) { + stream.getTracks().forEach((track) => track.stop()); + } + }; + }, [stream]); + + const getMedia = async () => { + try { + const constraints = { + audio: { + echoCancellation: { exact: true }, + }, + video: { + width: 1280, + height: 720, + }, + }; + const mediaStream = + await navigator.mediaDevices.getUserMedia(constraints); + setStream(mediaStream); + if (mirrorVideoRef.current) { + mirrorVideoRef.current.srcObject = mediaStream; + } + } catch (e) { + console.log(`현재 마이크와 카메라가 연결되지 않았습니다`); + } + }; + + const handleStartRecording = () => { + setRecordedBlobs([]); + try { + mediaRecorderRef.current = new MediaRecorder(stream as MediaStream, { + mimeType: selectedMimeType, + }); + mediaRecorderRef.current.ondataavailable = (event) => { + if (event.data && event.data.size > 0) { + setRecordedBlobs((prev) => [...prev, event.data]); + } + }; + mediaRecorderRef.current.start(); + setRecording(true); + } catch (e) { + console.log(`MediaRecorder error`); + } + }; + + const handleStopRecording = () => { + if (mediaRecorderRef.current) { + mediaRecorderRef.current.stop(); + } + setRecording(false); + }; + + const handleDownload = () => { + const blob = new Blob(recordedBlobs, { type: selectedMimeType }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = 'recorded.webm'; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); + }; + + const getSupportedMimeTypes = () => { + const types = [ + 'video/webm; codecs=vp8', + 'video/webm; codecs=vp9', + 'video/webm; codecs=h264', + 'video/mp4; codecs=h264', + ]; + return types.filter((type) => MediaRecorder.isTypeSupported(type)); + }; + return (
- 면접페이지의 카메라 입니다. +
); }; + export default InterviewCamera;