-
Notifications
You must be signed in to change notification settings - Fork 0
Fe/bugfix,refactor/#520 채팅 페이지를 나왔을 때 카메라 권한이 꺼지도록 수정 #546
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
The head ref may contain hidden characters: "#520-\uCC44\uD305-\uD398\uC774\uC9C0\uB97C-\uB098\uC654\uC744-\uB54C-\uCE74\uBA54\uB77C-\uAD8C\uD55C\uC774-\uAEBC\uC9C0\uB3C4\uB85D-\uC218\uC815"
Conversation
- 기존대로 하면 리렌더링이 돼야 로딩상태를 벗어남 - 문제가 있는 방법이기에 stream 상태를 기반으로 loading 상태를 줌
- 결국 sdp, ice를 주고 받는 과정인데 이벤트를 나눌 필요가 없었음 - 그래서 connection이라는 이벤트로 통일시켜줌 - 이렇게 한 이유는 재협상 할 때 이 이벤트를 사용하기 위해서임
- 이 전 리팩토링에서는 의존성을 한방향으로 풀어서 보기 쉽게 리팩토링함 - 이번에는 훅 안의 함수가 어떤 기능을 하는지 정확히 알 수 있도록 리팩토링함
- localStream의 트랙을 모두 stop함으로써 이 문제를 해결함
- zustand 목업에 MediaStream목업을 추가함
- rootDir/__mocks가 추가되어 있지 않아서 에러가 뜸 - 그래서 추가해줌
- await을 붙여주지 않았음
- 인수로 받는 replacePeerconnection를 사용하도록 변경
|
||
@SubscribeMessage<HumanClientEvent>('offer') | ||
handleOfferEvent( | ||
@SubscribeMessage<HumanClientEvent>('connection') | ||
handleConnectionEvent( | ||
socket: Socket, | ||
[sdp, roomName]: [RTCSessionDescription, string], | ||
{ | ||
description, | ||
candidate, | ||
roomName, | ||
}: { | ||
description?: RTCSessionDescription; | ||
candidate?: RTCIceCandidate; | ||
roomName: string; | ||
}, | ||
) { | ||
this.logger.debug(`🚀 Offer Received from ${socket.id}`); | ||
this.eventEmitToRoom(socket, roomName, 'offer', sdp); | ||
} | ||
|
||
@SubscribeMessage<HumanClientEvent>('answer') | ||
handleAnswerEvent( | ||
socket: Socket, | ||
[sdp, roomName]: [RTCSessionDescription, string], | ||
) { | ||
this.logger.debug(`🚀 Answer Received from ${socket.id}`); | ||
this.eventEmitToRoom(socket, roomName, 'answer', sdp); | ||
} | ||
|
||
@SubscribeMessage<HumanClientEvent>('candidate') | ||
handleCandidateEvent( | ||
socket: Socket, | ||
[candidate, roomName]: [RTCIceCandidate, string], | ||
) { | ||
this.logger.debug(`🚀 Candidate Received from ${socket.id}`); | ||
this.eventEmitToRoom(socket, roomName, 'candidate', candidate); | ||
try { | ||
if (description) { | ||
this.logger.debug(`🚀 ${description.type} Received from ${socket.id}`); | ||
this.eventEmitToRoom(socket, roomName, 'connection', { description }); | ||
} else if (candidate) { | ||
this.logger.debug(`🚀 Candidate Received from ${socket.id}`); | ||
this.eventEmitToRoom(socket, roomName, 'connection', { candidate }); | ||
} | ||
} catch (error) { | ||
this.logger.error(`🚀 Error in handleMessageEvent : ${error}`); | ||
} |
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.
💬 signalling 서버에서 sdp를 주고 받는 과정을 connection
이벤트 하나로 간소화함
-> 결국 description 혹은 candidate를 주고 받는 과정이기 때문에 하나로 합침
window.MediaStream = vi.fn().mockReturnValue({ | ||
getTracks: vi.fn().mockReturnValue([ | ||
{ enabled: true, id: 'test' }, | ||
{ enabled: true, id: 'test' }, | ||
]), | ||
}); | ||
|
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.
💬 만약 zustand를 초기화 할 때 MediaStream을 사용한다면 테스트를 진행할 수 없기에 zustand를 초기화 하기 이전에 MediaStream을 모킹해줌
{ type: 'audio', onOrOff: audioTrack?.enabled }, | ||
{ type: 'video', onOrOff: videoTrack?.enabled }, | ||
{ type: 'video', onOrOff: myVideoOn }, | ||
{ type: 'audio', onOrOff: myMicOn }, | ||
]), | ||
); | ||
} |
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.
💬 이제 권한을 끄면 enabled를 바꾸는게 아니라 아예 트랙을 제거 해버리기 때문에 트랙에서 가져오는게 아니라 상태에서 바로 가져오도록 수정함
const toggleMediaOnOff = async ({ | ||
type, | ||
replacePeerconnection = true, | ||
}: { | ||
type: 'audio' | 'video'; | ||
replacePeerconnection?: boolean; | ||
}) => { | ||
const mediaEnabled = type === 'video' ? myVideoEnabled : myMicEnabled; | ||
const toggleMediaState = type === 'video' ? toggleMyVideo : toggleMyMic; | ||
|
||
if (mediaInfoChannel?.readyState === 'open') { | ||
mediaInfoChannel.send(JSON.stringify([{ type, onOrOff: !mediaEnabled }])); | ||
} | ||
|
||
toggleMediaState(); | ||
|
||
if (mediaEnabled) { | ||
stopTracks(type); | ||
} else { | ||
const stream = type === 'video' ? await getVideoStream() : await getAudioStream(); | ||
|
||
setLocalStream(localStream); | ||
addTracks(stream, replacePeerconnection); | ||
} | ||
}; |
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.
💬 만약 type(audio | video)가 켜져있다면 stopTracks()
로 권한을 제거하고 트랙도 제거함
꺼져있다면 새롭게 카메라나 오디오를 얻어온 후 stream에 적용하고, peerConnection에도 적용해줌
const disconnectMediaStream = () => { | ||
localStream.getTracks().forEach(track => { | ||
track.stop(); | ||
localStream.removeTrack(track); | ||
}); | ||
}; |
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.
💬 MediaStream에 존재하는 모든 권한을 제거
export async function handleWebRTCConnectionSetup({ | ||
description, | ||
candidate, | ||
roomName, | ||
}: { roomName: string } & WebRTCConnectionSetupParams) { | ||
const webRTC = WebRTC.getInstance(HumanSocketManager.getInstance()); | ||
|
||
webRTC.setLocalDescription(answerSdp); | ||
// offer & answer를 주고받는 과정 | ||
if (description) { | ||
await webRTC.setRemoteDescription(description); | ||
|
||
socketManager.emit('answer', answerSdp, roomName); | ||
// 상대방이 보낸 offer에 대해 answer를 생성하고, 이를 다시 상대방에게 보냄 | ||
if (description.type === 'offer') { | ||
sendCreatedSDP(roomName, 'answer'); | ||
} | ||
} | ||
// 위의 과정이 끝나고 ice candidate를 주고받는 과정 | ||
else if (candidate) { | ||
await webRTC.addIceCandidate(candidate); | ||
} |
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.
💬 만약 connection
이벤트가 발동하면 이 함수가 실행돼서 offer, answer, candidate를 주고 받게됨
this.peerConnection.addEventListener('negotiationneeded', async () => { | ||
// 만약 아직 peerConnection이 초기화되지 않았거나, 연결된적이 없다면 리턴함 | ||
if (this.peerConnection?.signalingState !== 'stable' || !this.peerConnection?.remoteDescription) { | ||
return; | ||
} | ||
|
||
const offer = await this.peerConnection?.createOffer(); | ||
await this.setLocalDescription(offer); | ||
this.socketManager.emit('connection', { roomName, description: this.peerConnection?.localDescription }); | ||
}); |
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.
💬 이 부분이 재협상이 필요하다고 브라우저가 알려줬을 때 실행되는 핸들러
- offer를 생성하며 재협상 과정을 시작함
{loading && <div className="absolute skeleton w-h-full"></div>}{' '} | ||
{!stream?.active && <div className="absolute skeleton w-h-full rounded-[55px] "></div>} |
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.
💬 기존에 CamBox UI가 이상했던 부분을 여기서 수정해줌
- 스켈레톤에 rounded를 줘서video와 같은 모양을 만들어줌
- stream?.active를 사용해 실제로 stream이 오고 있지 않을때만 스켈레톤을 보이게 해줌
useEffect(() => { | ||
return () => { | ||
endWebRTC(); | ||
disconnectMediaStream(); | ||
}; | ||
}, []); |
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.
💬 HumanChatPage를 나갈 때 MediaStream의 모든 권한을 제거해줌
@@ -33,6 +33,6 @@ | |||
|
|||
}, | |||
"typeRoots": ["./node_modules/@types", "./src/@types"], | |||
"include": ["src", "@types", ".src/setup-vitest.ts"], | |||
"include": ["src", "@types", ".src/setup-vitest.ts", "__mocks__"], |
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.
💬 /mocks 안에 있는 폴더들은 vi를 인식하지 못하므로 추가해줌
@SubscribeMessage<HumanClientEvent>('offer') | ||
handleOfferEvent( | ||
@SubscribeMessage('connection') | ||
handleConnectionEvent( |
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.
💬 현재 HumanClientEvent를 npm에서 가져와 쓰고있는데 이럴경우 수정해야 할 때 큰 번거로움이 존재함
- 빌드가 되질 않아서 일단 저 타입을 사용하지 않음
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.
💬 이거 이번에 다른 방식으로 고쳐봐야겠다
- 그래서 일단 저 타입을 사용하지 않음
2be8e8f
to
7cd5a67
Compare
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.
WebRTC 천재 👍👍
@SubscribeMessage<HumanClientEvent>('offer') | ||
handleOfferEvent( | ||
@SubscribeMessage('connection') | ||
handleConnectionEvent( |
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.
💬 이거 이번에 다른 방식으로 고쳐봐야겠다
🔮 resolved #520
변경 사항
고민과 해결 과정
1️⃣채팅 페이지를 나왔을 때 카메라 권한이 꺼지도록 수정 / signalling server에서 offer, answer, candidate 주고 받는 함수 하나로 통일
리팩토링을 하기 이전의 진행과정은 아래와 같음
위의 1, 2, 3번이 모두 문제가 됐기에 관련 훅들을 모두 리팩토링 해줌
리팩토링을 하면서 사용자가 실제로 권한을 요청 했을때만 권한을 얻도록 수정함
또, 카메라나 마이크를 끄면 실제로 권한을 없애줌
그런데 카메라 권한을 얻은 후 peerConnection에 다시 추가하려면
재협상
과정이 필요했음재협상
은 offer, answer, candidate를 다시 주고 받는 과정인데 이 이벤트들을 나눠두면재협상
과정이 번거로웠음그래서
connection
이라는 하나의 이벤트로 합쳐줌그러고서
재협상
이 필요할 경우 아래의 이벤트 리스너로 그 과정을 진행해 주었음참조: addTrack을 할 경우 재협상이 필요
이제 권한을 얻을때는
navigator.mediaDevices.getUserMedia({ audio, video })
를 호출하면 되고권한을 제거할때는 위 getUserMedia 함수의 리턴값인 MediaStream.getTracks()로 track을 얻어와 track.stop()을 호출하면됨
결과
useMediaStream훅 안의 아래의 함수를 호출하면 모든 권한을 제거해줌
2️⃣WebRTC 한번 더 리팩토링
사실 전에 WebRTC를 리팩토링 했을때도 사용하는게 조금 까다로웠음
왜냐하면 MediaStream을 다루려면 useVideoRef, useControllMedia을 사용해야 했기 때문
따라서 useMediaStream에서 이를 모두 다룰 수 있도록 리팩토링 해줌.
그 결과 MediaStream을 통해 카메라나 마이크 권한을 얻거나 제거하는 등의 행위는 모두 useMediaStream을 통해 이뤄짐
(선택) 테스트 결과
2024-02-19.12.12.05.mov
ex) 베이스 브랜치에 포함되기 위한 코드는 모두 정상적으로 동작해야 합니다. 결과물에 대한 스크린샷, GIF, 혹은 라이브 데모가 가능하도록 샘플API를 첨부할 수도 있습니다.