Skip to content
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

Merged
merged 5 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions FE/src/GlobalSvgProvider.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p-5] 후후 대신 해결해주셔서 감사합니다~ 앞으로 머지할 때 주의할게요

Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ const spliteSvgCode = (
<path
d="M7.15964 4.50879C7.49297 4.50879 7.76797 4.78379 7.76797 5.11712C7.76797 5.45046 7.49297 5.72546 7.15964 5.72546C4.80964 5.72546 2.89297 7.64212 2.89297 9.99212C2.89297 12.3421 4.80964 14.2588 7.15964 14.2588C9.50964 14.2588 11.4263 12.3421 11.4263 9.99212C11.4263 9.65879 11.7013 9.38379 12.0346 9.38379C12.368 9.38379 12.643 9.65879 12.643 9.99212C12.643 13.0171 10.1846 15.4838 7.1513 15.4838C4.11797 15.4838 1.66797 13.0255 1.66797 10.0005C1.66797 6.97546 4.1263 4.50879 7.15964 4.50879Z"
fill="#292D32"
/>
</symbol>
<symbol id="edit" viewBox="0 0 32 32">
<path
d="M0 16C0 7.16344 7.16344 0 16 0C24.8366 0 32 7.16344 32 16C32 24.8366 24.8366 32 16 32C7.16344 32 0 24.8366 0 16Z"
Expand Down
40 changes: 40 additions & 0 deletions FE/src/hooks/useMedia.ts
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;
128 changes: 44 additions & 84 deletions FE/src/page/interviewPage/index.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p-5] before after가 눈물나게 아름답네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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';
Expand All @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The 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`);
    }
  };

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 네네 이건 지금 record hook 을빼면서 진행하려구 아껴(?)두고 있습니다

});
mediaRecorderRef.current.ondataavailable = (event) => {
Expand Down Expand Up @@ -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;
37 changes: 37 additions & 0 deletions FE/src/utils/media.ts
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[p-3] 요거는 너무 useMedia hook내부에 있는 mediaStream을 의존하고 있어서 util말고 hook쪽의 함수로 설정해도 될것 같은데 어떤가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이건 다른 의미도 있습니다!
예를들어 제가 화상으로 말씀드렸다 싶이, 제가 저 함수를 호출하는 시점은 useEffect로 컴포넌트가 unmount 될때 호출되고 있어요!
그런데 navigate로 인해서 라우터 처리를 진행할때는 unmount 로직이 실행되지 않는 문제가 발생했습니다.

공식문서를 찾아봤을때는 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));
};
Loading