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-87] Interview 페이지 camera 컴포넌트 기능 부여 (7h/8h) #30

Merged
merged 64 commits into from
Nov 11, 2023

Conversation

adultlee
Copy link
Collaborator

@adultlee adultlee commented Nov 10, 2023

NDD-87 Powered by Pull Request Badge

Why

Interview 페이지를 작업하기 위해서 Interview 페이지의 Camera 최소 기능을 구현합니다.

Camera 기능에는 다음과 같은 기능을 최소 기능으로 선정합니다.

  1. 녹화를 위해 media를 연결합니다.
  2. 녹화를 시작합니다.
  3. 녹화를 종료합니다.
  4. 녹화된 결과물을 저장합니다.

How

stream 요소 관리

  const [stream, setStream] = useState<MediaStream | null>(null);

현재 stream되는 요소를 state로서 관리합니다.
화면에서 바로 보이는 미디어에 대한 stream 을 관리할 수 있습니다.
해당 stream은 getMedia 함수의 useState를 통해서 관리됩니다.

        const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
      setStream(mediaStream);

recording 요소 관리

  const [recording, setRecording] = useState(false);

해당 컴포넌트 내부에서 "녹화" 상태를 확인합니다. 현재 컴포넌트의 가장 높은 상태에서 관리해야하며, 전역적으로 관리될 예정입니다.

recordedBlobs 요소 관리

  const [recordedBlobs, setRecordedBlobs] = useState<Blob[]>([]);

녹화가 되는 요소를 관리할 state입니다.
startRecord를 통해서 관리됩니다.

selectedMimeType 요소 관리

  const [selectedMimeType, setSelectedMimeType] = useState('');

현재 디바이스환경에서 사용할 수 있는 MimeType에 대해서 관리합니다.

getMedia 기능 (현재 컴포넌트에서 media 를 연결하는 기능)

  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 (gumVideoRef.current) {
        gumVideoRef.current.srcObject = mediaStream;
      }
    } catch (e) {
      console.log(`현재 마이크와 카메라가 연결되지 않았습니다`);
    }
  };

Media를 셋팅할 수 있는 constraits에 대해서 명시합니다.
비동기로 미디어의 연결을 받습니다.
try-catch로 확인하여 현재 미디어 연결을 받습니다.

handleStartRecording (현재 녹화를 시작하는 기능)

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

녹화를 시작하는 기능입니다.
처음 녹화를 시작할때, RecordBlobs를 초기화합니다,
그 후 선언한 mediaRecordRef에 새롭게 초기화 하며, MediaRecorder(Web Api) 생성자를 선언합니다.

handleStopRecording (녹화 종료)

const handleStopRecording = () => {
    if (mediaRecorderRef.current) {
      mediaRecorderRef.current.stop();
    }
    setRecording(false);
  };

현재 녹화중인 mediaRecordRef에 대해서 정리합니다.
여기서 중요한 점은 gumVideoRef 현 시점에서 송출하고 있는 Media는 접근하지 않습니다.

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);

Media 타입을 선정

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

Result

recorded.2.webm

Reference

https://medium.com/watcha/%EC%9B%B9%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%97%90%EC%84%9C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94-%EB%85%B9%ED%99%94%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-70142ce28994

@milk717 milk717 added FE 프론트엔드 코드 변경사항 feature 새로운 기능이 추가 된 경우 labels Nov 10, 2023
Copy link

cloudflare-workers-and-pages bot commented Nov 11, 2023

Deploying with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4049497
Status: ✅  Deploy successful!
Preview URL: https://49706ddb.gomterview.pages.dev
Branch Preview URL: https://feautre-ndd-87.gomterview.pages.dev

View logs

setSelectedMimeType(mimeTypes[0]);
}
}, []);

Copy link
Collaborator

Choose a reason for hiding this comment

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

[p-1] 해당 로직은 필요가 없다고 생각합니다. selectedMineType 함수를 사용하고 있는데 이제 초기값을 받기 위해서 사용하는 로직이라면 state를 사용하지 않고 변수로 사용하는 방법으로 적용해도 똑같이 사용이 가능하다고 생각합니다.

  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));
  };
  const selectedMimeType = getSupportedMimeTypes();

이런식으로요
useLayoutEffect는 랜더링 사이클에서 많은 비용을 소모하고 있어서 사용을 지양하는것이 좋으니 한번 확인해 보시고 수정하는건 어떤가요?

const handleStartRecording = () => {
setRecordedBlobs([]);
try {
mediaRecorderRef.current = new MediaRecorder(stream as MediaStream, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder

해당 코드에 오디오, 비디오 bitrate option을 설정해줄 수 있습니다.
v:3mbps, a: 128kbps

const option = {
   audioBitsPerSecond: 128000,
   videoBitsPerSecond: 3000000,
   mimeType: "video/mp4",
}

new MediaRecorder(stream, options);

Comment on lines +20 to +30
useEffect(() => {
if (!stream) {
void getMedia();
}

return () => {
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
};
}, [stream]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

(구두로 이야기했지만 메모해둔 내용!)
useEffect의 return에 있는 로직이 컴포넌트가 언마운트 될 때만 실행되는 것이 아니라 종속성 배열에 있는 상태에 대한 사이클이 변경될 때 실행되는 것이라서 위 코드에서 stream이 변경될 때 마다 useEffect가 실행되는 사이클이 일어날 것이라고 생각했습니다.
하지만 현재 로직에서 stream은 두번의 변경이 일어나고, 이마저도 if문으로 막혀있어서 계속해서 실행되는 문제는 없을 것이라고 판단했습니다!

Copy link
Collaborator

@Yoon-Hae-Min Yoon-Hae-Min left a comment

Choose a reason for hiding this comment

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

web api는 너무 어려운것 같아요 ㅠㅠ 고생 많으셨습니다

Copy link
Collaborator

@milk717 milk717 left a comment

Choose a reason for hiding this comment

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

제일 까다로운 작업 하시느냐고 고생하셨습니다!
web api에서 이렇게까지 많이 지원해주는지 몰랐네요
덕분에 많이 배워갑니다~

@adultlee adultlee merged commit 715de35 into dev Nov 11, 2023
1 check passed
@delete-merged-branch delete-merged-branch bot deleted the feautre/NDD-87 branch November 11, 2023 10:33
adultlee added a commit that referenced this pull request Nov 11, 2023
* [NDD-103] Member API E2E 테스트 (1h / 1h) (#29)

* test: Member API E2E 테스트 코드 작성

1. /api/member GET API 에 대한 성공 테스트
2. /api/member GET API에서 유효하지 않은 토큰 사용으로 인한 실패 테스트
위의 2가지의 경우에 대한 테스트 코드 작성

* style: lint 적용

* test: 인수 테스트 코드 로직 변경 완료

GET /api/member에 대한 E2E 테스트 완료

* style: Window 개행문자 관련 에러 해결을 위한 lint 설정 추가

* feat: TestingModule을 생성하는 Util 메서드 구현

* [NDD-87] Interview 페이지 camera 컴포넌트 기능 부여 (7h/8h) (#30)

* 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 이름 변경

---------

Co-authored-by: quiet-honey <99426344+quiet-honey@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
FE 프론트엔드 코드 변경사항 feature 새로운 기능이 추가 된 경우
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants