Skip to content

Commit 1ae7ba0

Browse files
authored
Feat/#339-S : RTC hook 버전 구현 (#345)
* feat : 시그널링 서버 구축 * chore : 필요없는 socket room join은 제거 * setting : socket.io-client 설치 * feat : ConfBar 기본 틀만 구성 * feat : 소켓 메세지 정의 * feat : 소켓 연동을 위한 useSocket hook 구현 * feat : WebRTC 시그널링 구현 (테스트전) socket 이벤트 연동 - join, offer, answer, ice-candidate setMyStream: 내 비디오 설정 함수 분리 setPeerConnection: 각각의 피어 연결을 위한 세팅 함수 분리 IParticipant: 참여자들 타입 정의 -> 아직 뭐가 더 필요한지 모르겠음.. * feat : users 배열에 socketId 별로 구분하여 참여 유저 관리 * fix : 임시 fetch 로 바꾸기 axios의 인코딩 이슈로 인해.. * feat : 소켓 시그널링 시 주고 받는 값들(senderId, receiveId) 설정 * feat : ConfBar 에 participants 붙이기 * feat : Video 컴포넌트 제작 * fix : workspace list가 없을 경우 0번째 Id로 이동하도록 해결 * temp * feat : 시그널링 모듈 구축 및 연동 미완성 Co-authored-by: Won-hee Cho <[email protected]> * chore : 콘솔 지우기 * fix : #114와 충돌 해결 * fix : #114와 충돌 해결 * chore : ConfBar 폴더명 일치 시키기 * refactor : 소켓을 워크스페이스 페이지로 빼기 * chore : 콘솔 제거 * chore : 기존 주석 제거 * refactor : 서버에서 관리하던 user 객체 제거 disconnect 이벤트 추가 * feat : 회의 시작 전 내 스트림 설정하는 모달 추가 * feat : RTC hook 버전 구현 * chore : dev 브랜치 버전으로 되돌리기 * chore : @import 구문 삭제 * chore : 이전 버전 useRTC 삭제 * chore : package-lock.json 충돌 해결 * feat : rtc Context 파일 추가 * feat : rtc Context 적용
1 parent 6400376 commit 1ae7ba0

File tree

8 files changed

+335
-1
lines changed

8 files changed

+335
-1
lines changed

@wabinar/constants/socket-message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const WORKSPACE_EVENT = {
1313
VIDEO_STATE_CHANGED: 'video-state-changed',
1414
SEND_BYE: 'send-bye',
1515
RECEIVE_BYE: 'receive-bye',
16+
EXISTING_ROOM_USERS: 'existing-room-users',
1617
};
1718

1819
export const MOM_EVENT = {

client/src/constants/rtc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const STUN_SERVER = ['stun:stun.l.google.com:19302'];
1+
export const STUN_SERVER = ['stun:stun.l.google.com:19302'];

client/src/contexts/rtc.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
createContext,
3+
Dispatch,
4+
MutableRefObject,
5+
SetStateAction,
6+
} from 'react';
7+
8+
export interface IMyMediaStreamContext {
9+
myStreamRef: MutableRefObject<MediaStream | undefined>;
10+
myMediaStream: MediaStream;
11+
setMyMediaStream: Dispatch<SetStateAction<MediaStream>>;
12+
isMyMicOn: boolean;
13+
setIsMyMicOn: Dispatch<SetStateAction<boolean>>;
14+
isMyCamOn: boolean;
15+
setIsMyCamOn: Dispatch<SetStateAction<boolean>>;
16+
}
17+
18+
export type TUserStreams = {
19+
[key: string]: MediaStream | null;
20+
};
21+
22+
export type TConnectedUser = {
23+
name: string;
24+
uid: number;
25+
sid: string;
26+
avatarUrl: string;
27+
};
28+
29+
export interface IUserStreamContext {
30+
userStreams: TUserStreams | null;
31+
setUserStreams: Dispatch<SetStateAction<TUserStreams | null>>;
32+
connectedUsers: TConnectedUser[];
33+
setConnectedUsers: Dispatch<SetStateAction<TConnectedUser[]>>;
34+
}
35+
36+
export const MyMediaStreamContext = createContext<IMyMediaStreamContext | null>(
37+
null,
38+
);
39+
40+
export const UserStreamsContext = createContext<IUserStreamContext | null>(
41+
null,
42+
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useContext } from 'react';
2+
import ERROR_MESSAGE from 'src/constants/error-message';
3+
import { MyMediaStreamContext } from 'src/contexts/rtc';
4+
5+
export default function useMyMediaStreamContext() {
6+
const context = useContext(MyMediaStreamContext);
7+
8+
if (!context) throw new Error(ERROR_MESSAGE.OUT_OF_CONTEXT_SCOPE);
9+
10+
return context;
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useContext } from 'react';
2+
import ERROR_MESSAGE from 'src/constants/error-message';
3+
import { UserStreamsContext } from 'src/contexts/rtc';
4+
5+
export default function useUserStreamsContext() {
6+
const context = useContext(UserStreamsContext);
7+
8+
if (!context) throw new Error(ERROR_MESSAGE.OUT_OF_CONTEXT_SCOPE);
9+
10+
return context;
11+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import useMyMediaStreamContext from './context/useMyMediaStreamContext';
2+
3+
export const useCreateMediaStream = () => {
4+
const { myMediaStream, setMyMediaStream, setIsMyMicOn, setIsMyCamOn } =
5+
useMyMediaStreamContext();
6+
7+
const toggleAudioStream = (enabled: boolean) => {
8+
if (myMediaStream) {
9+
myMediaStream.getAudioTracks().forEach((track) => {
10+
track.enabled = !enabled;
11+
});
12+
}
13+
setIsMyMicOn(enabled);
14+
setMyMediaStream(new MediaStream());
15+
};
16+
17+
const toggleVideoStream = (enabled: boolean) => {
18+
if (myMediaStream) {
19+
myMediaStream.getVideoTracks().forEach((track) => {
20+
myMediaStream.removeTrack(track);
21+
track.stop();
22+
});
23+
}
24+
setIsMyCamOn(enabled);
25+
setMyMediaStream(new MediaStream());
26+
};
27+
28+
const createAudioStream = async () => {
29+
try {
30+
const audioStream = await navigator.mediaDevices.getUserMedia({
31+
audio: true,
32+
});
33+
34+
myMediaStream.addTrack(audioStream.getAudioTracks()[0]);
35+
36+
setIsMyMicOn(true);
37+
setMyMediaStream(myMediaStream);
38+
} catch (error) {
39+
console.debug('failed to get audio stream', error);
40+
}
41+
};
42+
43+
const createVideoStream = async () => {
44+
try {
45+
const videoStream = await navigator.mediaDevices.getUserMedia({
46+
video: true,
47+
});
48+
49+
myMediaStream.addTrack(videoStream.getVideoTracks()[0]);
50+
51+
setIsMyCamOn(true);
52+
setMyMediaStream(myMediaStream);
53+
} catch (error) {
54+
console.debug('failed to get video stream', error);
55+
}
56+
};
57+
58+
return {
59+
toggleAudioStream,
60+
toggleVideoStream,
61+
createAudioStream,
62+
createVideoStream,
63+
};
64+
};

client/src/hooks/useJoinMeeting.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { WORKSPACE_EVENT } from '@wabinar/constants/socket-message';
2+
import { useEffect } from 'react';
3+
import { TConnectedUser } from 'src/contexts/rtc';
4+
5+
import useMyMediaStreamContext from './context/useMyMediaStreamContext';
6+
import useSocketContext from './context/useSocketContext';
7+
import useUserContext from './context/useUserContext';
8+
import useUserStreamContext from './context/useUserStreamsContext';
9+
10+
export default function useJoinMeeting() {
11+
const { workspaceSocket } = useSocketContext();
12+
const { user } = useUserContext();
13+
const { setConnectedUsers } = useUserStreamContext();
14+
const { setUserStreams } = useUserStreamContext();
15+
const { setMyMediaStream, myStreamRef } = useMyMediaStreamContext();
16+
17+
useEffect(() => {
18+
workspaceSocket.emit(WORKSPACE_EVENT.SEND_HELLO, user?.id);
19+
20+
const onExistingUsers = (users: TConnectedUser[]) => {
21+
const existingUsers = users.map((user) => ({
22+
...user,
23+
}));
24+
25+
setConnectedUsers(existingUsers);
26+
};
27+
28+
const onExitUser = (sid: string) => {
29+
setUserStreams((prev) => {
30+
delete prev?.[sid];
31+
return prev;
32+
});
33+
34+
setConnectedUsers((prev) => prev.filter((user) => user.sid !== sid));
35+
};
36+
37+
workspaceSocket.on(WORKSPACE_EVENT.EXISTING_ROOM_USERS, onExistingUsers);
38+
workspaceSocket.on(WORKSPACE_EVENT.RECEIVE_BYE, onExitUser);
39+
40+
return () => {
41+
workspaceSocket.off(WORKSPACE_EVENT.EXISTING_ROOM_USERS);
42+
workspaceSocket.off(WORKSPACE_EVENT.RECEIVE_BYE);
43+
44+
setUserStreams(null);
45+
setConnectedUsers([]);
46+
setMyMediaStream(new MediaStream());
47+
myStreamRef.current = undefined;
48+
};
49+
}, []);
50+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { WORKSPACE_EVENT } from '@wabinar/constants/socket-message';
2+
import { useEffect, useRef } from 'react';
3+
4+
import { User } from './../types/user.d';
5+
import useMyMediaStreamContext from './context/useMyMediaStreamContext';
6+
import useSocketContext from './context/useSocketContext';
7+
import useUserStreamContext from './context/useUserStreamsContext';
8+
9+
const usePeerConnection = () => {
10+
const { myMediaStream } = useMyMediaStreamContext();
11+
const { userStreams, setUserStreams, setConnectedUsers } =
12+
useUserStreamContext();
13+
const pcsRef = useRef<{ [socketId: string]: RTCPeerConnection }>({});
14+
const { workspaceSocket } = useSocketContext();
15+
16+
useEffect(() => {
17+
const createPeerConnection = (sid: string) => {
18+
try {
19+
const RTCConfig = {
20+
iceServers: [
21+
{
22+
urls: [
23+
'stun:stun.l.google.com:19302',
24+
'stun:stun1.l.google.com:19302',
25+
'stun:stun2.l.google.com:19302',
26+
'stun:stun3.l.google.com:19302',
27+
'stun:stun4.l.google.com:19302',
28+
],
29+
},
30+
],
31+
};
32+
33+
const peerConnection = new RTCPeerConnection(RTCConfig);
34+
35+
peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
36+
if (event.candidate) {
37+
workspaceSocket.emit(
38+
WORKSPACE_EVENT.SEND_ICE,
39+
event.candidate,
40+
sid,
41+
);
42+
}
43+
};
44+
45+
peerConnection.ontrack = (event: RTCTrackEvent) => {
46+
const stream = event.streams[0];
47+
48+
setUserStreams((prev) => ({ ...prev, [sid]: stream }));
49+
};
50+
51+
myMediaStream?.getTracks().forEach((track) => {
52+
peerConnection.addTrack(track, myMediaStream);
53+
});
54+
55+
peerConnection.onnegotiationneeded = async () => {
56+
try {
57+
const offer = await peerConnection.createOffer();
58+
await peerConnection.setLocalDescription(offer);
59+
60+
workspaceSocket.emit(WORKSPACE_EVENT.SEND_OFFER, offer, sid);
61+
} catch (err) {
62+
console.error(err);
63+
}
64+
};
65+
66+
return peerConnection;
67+
} catch (err) {
68+
console.debug(err);
69+
}
70+
};
71+
72+
const onReceivedOffer = async (
73+
offer: RTCSessionDescriptionInit,
74+
sid: string,
75+
) => {
76+
const pc = createPeerConnection(sid);
77+
if (!pc) return;
78+
79+
pcsRef.current = { ...pcsRef.current, [sid]: pc };
80+
81+
try {
82+
await pc.setRemoteDescription(offer);
83+
const answer = await pc.createAnswer();
84+
await pc.setLocalDescription(answer);
85+
86+
workspaceSocket.emit(WORKSPACE_EVENT.SEND_ANSWER, answer, sid);
87+
} catch (err) {
88+
console.debug(err);
89+
}
90+
};
91+
92+
const onReceivedAnswer = async (
93+
answer: RTCSessionDescriptionInit,
94+
sid: string,
95+
) => {
96+
const pc = pcsRef.current?.[sid];
97+
98+
if (!pc) return;
99+
await pc.setRemoteDescription(answer);
100+
};
101+
102+
const onReceivedIceCandidate = async (
103+
iceCandidate: RTCIceCandidateInit,
104+
sid: string,
105+
) => {
106+
const pc = pcsRef.current?.[sid];
107+
108+
if (!pc) return;
109+
await pc.addIceCandidate(iceCandidate);
110+
};
111+
112+
const onReceivedUser = async (sid: string, user: User) => {
113+
const pc = createPeerConnection(sid);
114+
115+
if (!pc) return;
116+
117+
const offer = await pc.createOffer();
118+
await pc.setLocalDescription(offer);
119+
120+
pcsRef.current = { ...pcsRef.current, [sid]: pc };
121+
122+
setConnectedUsers((prev) => [
123+
...prev,
124+
{
125+
sid: sid,
126+
uid: user.id,
127+
name: user.name,
128+
avatarUrl: user.avatarUrl,
129+
},
130+
]);
131+
132+
workspaceSocket.emit(WORKSPACE_EVENT.SEND_OFFER, offer, sid);
133+
};
134+
135+
workspaceSocket.on(WORKSPACE_EVENT.RECEIVE_HELLO, onReceivedUser);
136+
workspaceSocket.on(WORKSPACE_EVENT.RECEIVE_OFFER, onReceivedOffer);
137+
workspaceSocket.on(WORKSPACE_EVENT.RECEIVE_ANSWER, onReceivedAnswer);
138+
workspaceSocket.on(WORKSPACE_EVENT.RECEIVE_ICE, onReceivedIceCandidate);
139+
140+
return () => {
141+
workspaceSocket.off(WORKSPACE_EVENT.RECEIVE_OFFER);
142+
workspaceSocket.off(WORKSPACE_EVENT.RECEIVE_ANSWER);
143+
workspaceSocket.off(WORKSPACE_EVENT.RECEIVE_ICE);
144+
145+
if (userStreams) {
146+
Object.keys(userStreams).forEach((sid) => {
147+
pcsRef.current?.[sid].close();
148+
delete pcsRef.current[sid];
149+
});
150+
}
151+
};
152+
}, [userStreams, myMediaStream]);
153+
};
154+
155+
export default usePeerConnection;

0 commit comments

Comments
 (0)