-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NDD-237] Interview 페이지의 useMedia hooks 개발 (2h/2h) #86
Changes from all commits
f3fabd5
79e5a5b
f1638c2
e8b0880
548a6ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { closeMedia, getMedia, getSupportedMimeTypes } from '@/utils/media'; | ||
import { useState, useEffect, useCallback, useRef } from 'react'; | ||
|
||
const useMedia = () => { | ||
const [media, setMedia] = useState<MediaStream | null>(null); | ||
const [selectedMimeType, setSelectedMimeType] = useState(''); | ||
const [connectStatus, setIsConnectedStatus] = useState< | ||
'connect' | 'fail' | 'pending' | ||
>('pending'); | ||
const videoRef = useRef<HTMLVideoElement>(null); | ||
|
||
const connectMedia = useCallback(async () => { | ||
try { | ||
const media = await getMedia(); | ||
|
||
setMedia(media); | ||
if (media) setIsConnectedStatus('connect'); | ||
else setIsConnectedStatus('fail'); | ||
if (videoRef.current) videoRef.current.srcObject = media; | ||
} catch (e) { | ||
console.log(`현재 마이크와 카메라가 연결되지 않았습니다`); | ||
} | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (!media) { | ||
void connectMedia(); | ||
} | ||
const mimeTypes = getSupportedMimeTypes(); | ||
if (mimeTypes.length > 0) setSelectedMimeType(mimeTypes[0]); | ||
|
||
return () => { | ||
closeMedia(media); | ||
}; | ||
}, [media, connectMedia]); | ||
|
||
return { media, videoRef, connectStatus, selectedMimeType }; | ||
}; | ||
|
||
export default useMedia; |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [p-5] before after가 눈물나게 아름답네요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제 생각도 그렇습니다😂 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import React, { useState, useRef, useEffect } from 'react'; | ||
import React, { useState, useRef } from 'react'; | ||
|
||
import InterviewPageLayout from '@components/interviewPage/InterviewPageLayout'; | ||
import InterviewHeader from '@/components/interviewPage/InterviewHeader/InterviewHeader'; | ||
|
@@ -14,64 +14,36 @@ import { PATH } from '@constants/path'; | |
import { Navigate } from 'react-router-dom'; | ||
import { useRecoilValue } from 'recoil'; | ||
import { recordSetting } from '@/atoms/interviewSetting'; | ||
import useMedia from '@/hooks/useMedia'; | ||
|
||
import { useNavigate } from 'react-router-dom'; | ||
const InterviewPage: React.FC = () => { | ||
const isAllSuccess = useIsAllSuccess(); | ||
const { method } = useRecoilValue(recordSetting); | ||
|
||
const isLogin = useQueryClient().getQueryState(QUERY_KEY.MEMBER); | ||
const navigate = useNavigate(); | ||
const { currentQuestion, getNextQuestion, isLastQuestion } = | ||
useInterviewFlow(); | ||
|
||
const [stream, setStream] = useState<MediaStream | null>(null); | ||
const { | ||
media, | ||
videoRef: mirrorVideoRef, | ||
connectStatus, | ||
selectedMimeType, | ||
} = useMedia(); | ||
|
||
const [isRecording, setIsRecording] = useState(false); | ||
const [isScriptInView, setIsScriptInView] = useState(true); | ||
const [recordedBlobs, setRecordedBlobs] = useState<Blob[]>([]); | ||
const [selectedMimeType, setSelectedMimeType] = useState(''); | ||
const [interviewIntroModalIsOpen, setInterviewIntroModalIsOpen] = | ||
useState<boolean>(true); | ||
|
||
const mirrorVideoRef = useRef<HTMLVideoElement>(null); | ||
const mediaRecorderRef = useRef<MediaRecorder | null>(null); | ||
|
||
useEffect(() => { | ||
if (!stream && isAllSuccess) { | ||
void getMedia(); | ||
} | ||
const mimeTypes = getSupportedMimeTypes(); | ||
if (mimeTypes.length > 0) setSelectedMimeType(mimeTypes[0]); | ||
|
||
return () => { | ||
if (stream) { | ||
// recoil 을 모두 초기화 | ||
stream.getTracks().forEach((track) => track.stop()); | ||
} | ||
}; | ||
}, [isAllSuccess, stream]); | ||
|
||
const getMedia = async () => { | ||
try { | ||
const mediaStream = await navigator.mediaDevices.getUserMedia({ | ||
audio: { | ||
echoCancellation: { exact: true }, | ||
}, | ||
video: { | ||
width: 1280, | ||
height: 720, | ||
}, | ||
}); | ||
|
||
setStream(mediaStream); | ||
if (mirrorVideoRef.current) | ||
mirrorVideoRef.current.srcObject = mediaStream; | ||
} catch (e) { | ||
console.log(`현재 마이크와 카메라가 연결되지 않았습니다`); | ||
} | ||
}; | ||
|
||
const handleStartRecording = () => { | ||
try { | ||
mediaRecorderRef.current = new MediaRecorder(stream as MediaStream, { | ||
mediaRecorderRef.current = new MediaRecorder(media as MediaStream, { | ||
mimeType: selectedMimeType, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [p-3] 요 부분은 media가 MediaStream으로 추론되지 않아서라기 보단 media가 null 일수도 있어서 발생하는 ts 경고이기 때문에 아래와 같은 형식으로 처리하는 것은 어떨까요? const handleStartRecording = () => {
try {
if (!media) {
return;
}
mediaRecorderRef.current = new MediaRecorder(media, {
mimeType: selectedMimeType,
});
mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
setRecordedBlobs([event.data]);
}
};
mediaRecorderRef.current.start();
setIsRecording(true);
// RecordStartingTime 을 초기화합니다.
// pre-signed url을 받습니다.
} catch (e) {
console.log(`MediaRecorder error`);
}
}; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 네네 이건 지금 record hook 을빼면서 진행하려구 아껴(?)두고 있습니다 |
||
}); | ||
mediaRecorderRef.current.ondataavailable = (event) => { | ||
|
@@ -129,50 +101,38 @@ const InterviewPage: React.FC = () => { | |
setRecordedBlobs([]); | ||
}; | ||
|
||
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)); | ||
}; | ||
|
||
if (!isAllSuccess) return <Navigate to={PATH.ROOT} />; | ||
|
||
return ( | ||
<InterviewPageLayout> | ||
<InterviewHeader | ||
isRecording={isRecording} | ||
intervieweeName="가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하" | ||
/> | ||
<InterviewMain | ||
mirrorVideoRef={mirrorVideoRef} | ||
isScriptInView={isScriptInView} | ||
question={currentQuestion.questionContent} | ||
answer={currentQuestion.answerContent} | ||
/> | ||
<InterviewFooter | ||
isRecording={isRecording} | ||
recordedBlobs={recordedBlobs} | ||
isLastQuestion={isLastQuestion} | ||
handleStartRecording={handleStartRecording} | ||
handleStopRecording={handleStopRecording} | ||
handleScript={() => setIsScriptInView((prev) => !prev)} | ||
handleNextQuestion={getNextQuestion} | ||
handleDownload={handleDownload} | ||
/> | ||
<InterviewIntroModal | ||
isOpen={interviewIntroModalIsOpen} | ||
closeModal={() => setInterviewIntroModalIsOpen((prev) => !prev)} | ||
/> | ||
<InterviewTimeOverModal | ||
isOpen={false} | ||
closeModal={() => console.log('모달을 종료합니다.')} | ||
/> | ||
</InterviewPageLayout> | ||
); | ||
if (!isAllSuccess || connectStatus === 'fail') { | ||
return <Navigate to={PATH.ROOT} />; | ||
} else | ||
return ( | ||
<InterviewPageLayout> | ||
<InterviewHeader isRecording={isRecording} intervieweeName="면접자" /> | ||
<InterviewMain | ||
mirrorVideoRef={mirrorVideoRef} | ||
isScriptInView={isScriptInView} | ||
question={currentQuestion.questionContent} | ||
answer={currentQuestion.answerContent} | ||
/> | ||
<InterviewFooter | ||
isRecording={isRecording} | ||
recordedBlobs={recordedBlobs} | ||
isLastQuestion={isLastQuestion} | ||
handleStartRecording={handleStartRecording} | ||
handleStopRecording={handleStopRecording} | ||
handleScript={() => setIsScriptInView((prev) => !prev)} | ||
handleNextQuestion={getNextQuestion} | ||
handleDownload={handleDownload} | ||
/> | ||
<InterviewIntroModal | ||
isOpen={interviewIntroModalIsOpen} | ||
closeModal={() => setInterviewIntroModalIsOpen((prev) => !prev)} | ||
/> | ||
<InterviewTimeOverModal | ||
isOpen={false} | ||
closeModal={() => console.log('모달을 종료합니다.')} | ||
/> | ||
</InterviewPageLayout> | ||
); | ||
}; | ||
|
||
export default InterviewPage; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
export const closeMedia = (media: MediaStream | null) => { | ||
if (media) { | ||
media.getTracks().forEach((track) => track.stop()); | ||
} | ||
}; | ||
Comment on lines
+1
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [p-3] 요거는 너무 useMedia hook내부에 있는 mediaStream을 의존하고 있어서 util말고 hook쪽의 함수로 설정해도 될것 같은데 어떤가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 이건 다른 의미도 있습니다! 공식문서를 찾아봤을때는 navigate를 통해서 라우터 처리를 진행할때, react 생명주기에 간섭하는 경우가 발생해 원하는 동작을 못하는 경우가 왕왕 발생한다구 하더라구요! 그래서 navigate를 사용하는 위치에서 해당 함수를 따로 import를 받아서 처리하면 어떨까 싶었습니다! 내부에 크게 의존하고 있지는 않습니다 사실 인자로 받은 media를 종료시키는 기능만을 담고 있으니까요! |
||
|
||
export const getMedia = async (): Promise<MediaStream | null> => { | ||
try { | ||
const media = await navigator.mediaDevices.getUserMedia({ | ||
audio: { | ||
echoCancellation: { exact: true }, | ||
}, | ||
video: { | ||
width: 1280, | ||
height: 720, | ||
}, | ||
}); | ||
|
||
return media; | ||
} catch (error) { | ||
alert( | ||
'현재 브라우저에 카메라 및 마이크가 연결되지 않았습니다. 카메라 및 마이크의 접근 권한의 재설정 후 서비스를 이용하실 수 있습니다.' | ||
); | ||
|
||
return null; | ||
} | ||
}; | ||
|
||
export 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)); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[p-5] 후후 대신 해결해주셔서 감사합니다~ 앞으로 머지할 때 주의할게요