Skip to content

Commit e37b792

Browse files
juyeong-swcho21
andauthored
Feat/temp-C : WebRTC 시그널링 구현 (#127)
- resolve #109 - resolve #114 - resolve #120 Co-authored-by: W. Cho <[email protected]>
1 parent 03cd9ac commit e37b792

File tree

16 files changed

+389
-11
lines changed

16 files changed

+389
-11
lines changed

client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"react": "^18.2.0",
1616
"react-dom": "^18.2.0",
1717
"react-loader-spinner": "^5.3.4",
18-
"react-router-dom": "^6.4.3"
18+
"react-router-dom": "^6.4.3",
19+
"socket.io-client": "^4.5.4"
1920
},
2021
"devDependencies": {
2122
"@testing-library/jest-dom": "^5.16.5",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useEffect, useRef } from 'react';
2+
3+
import style from './style.module.scss';
4+
5+
interface MediaProps {
6+
stream: MediaStream;
7+
}
8+
9+
function ConfMedia({ stream }: MediaProps) {
10+
const ref = useRef<HTMLVideoElement>(null);
11+
12+
useEffect(() => {
13+
if (!ref.current) return;
14+
ref.current.srcObject = stream;
15+
}, [stream]);
16+
17+
return <video className={style.video} ref={ref} autoPlay />;
18+
}
19+
20+
export default ConfMedia;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.video {
2+
width: 240px;
3+
height: 180px;
4+
border-radius: 20px;
5+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import ConfMedia from './ConfMedia';
2+
import style from './style.module.scss';
3+
4+
interface ConfMediaBarProps {
5+
streams: Map<string, MediaStream>;
6+
}
7+
8+
function ConfMediaBar({ streams }: ConfMediaBarProps) {
9+
return (
10+
<div className={style['conf-bar']}>
11+
<ul>
12+
{Array.from(streams).map(([id, stream]) => (
13+
<li key={id}>
14+
<ConfMedia key={id} stream={stream} />
15+
</li>
16+
))}
17+
</ul>
18+
</div>
19+
);
20+
}
21+
22+
export default ConfMediaBar;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.conf-bar {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
width: fit-content;
6+
min-width: 300px;
7+
height: 100vh;
8+
padding: 30px 20px;
9+
overflow: hidden;
10+
11+
ul {
12+
height: 100%;
13+
padding: 0 10px;
14+
overflow: hidden;
15+
overflow-y: auto;
16+
17+
li:not(:last-child) {
18+
margin-bottom: 10px;
19+
}
20+
}
21+
}

client/src/constants/rtc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const STUN_SERVER = ['stun:stun.l.google.com:19302'];
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useEffect, useState } from 'react';
2+
import { Socket, io } from 'socket.io-client';
3+
import { STUN_SERVER } from 'src/constants/rtc';
4+
import RTC from 'src/utils/rtc';
5+
6+
export function useConfMediaStreams(socket: Socket) {
7+
const [mediaStreams, setMediaStreams] = useState<Map<string, MediaStream>>(new Map());
8+
9+
useEffect(() => {
10+
(async () => {
11+
const userMedia = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
12+
setMediaStreams(prev => copyMapWithOperation(prev, map => map.set('me', userMedia)));
13+
14+
const rtc = new RTC(socket, STUN_SERVER, userMedia);
15+
rtc.onMediaConnected((socketId, remoteStream) => {
16+
setMediaStreams(prev => copyMapWithOperation(prev, map => map.set(socketId, remoteStream)));
17+
});
18+
rtc.onMediaDisconnected((socketId) => {
19+
setMediaStreams(prev => copyMapWithOperation(prev, map => map.delete(socketId)));
20+
});
21+
rtc.connect();
22+
})();
23+
}, [])
24+
25+
return mediaStreams;
26+
}
27+
28+
// TODO: 코드 반복때문에 만든 함수. 더 좋은 방법 있으면 고치기
29+
function copyMapWithOperation<K, V>(prev: Map<K, V>, operation: (cur: Map<K, V>) => void) {
30+
const cur = new Map(prev);
31+
operation(cur);
32+
return cur;
33+
}

client/src/main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { BrowserRouter } from 'react-router-dom';
55
import App from './App';
66

77
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
8-
<React.StrictMode>
8+
// <React.StrictMode>
99
<BrowserRouter>
1010
<App />
1111
</BrowserRouter>
12-
</React.StrictMode>,
12+
// </React.StrictMode>,
1313
);

client/src/pages/Workspace/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,24 @@ import Workspace from 'components/Workspace';
22
import WorkspaceList from 'components/WorkspaceList';
33
import { useEffect, useState } from 'react';
44
import { useParams } from 'react-router-dom';
5+
import { io } from 'socket.io-client';
56
import { getWorkspaceInfo } from 'src/apis/workspace';
7+
import ConfMediaBar from 'src/components/ConfMediaBar';
8+
import config from 'src/config';
9+
import { useConfMediaStreams } from 'src/hooks/useConfMediaStreams';
610
import { WorkspaceInfo } from 'src/types/workspace';
711

812
import style from './style.module.scss';
913

1014
function WorkspacePage() {
11-
const { id } = useParams();
1215
const [workspace, setWorkspace] = useState<WorkspaceInfo | null>(null);
1316

17+
const { id } = useParams();
18+
const socketUrl = `${config.SERVER_PATH}/signaling/${id}`;
19+
const socket = io(socketUrl);
20+
21+
const confMediaStreams = useConfMediaStreams(socket);
22+
1423
const loadWorkspaceInfo = async () => {
1524
if (id) {
1625
const workspaceInfo = await getWorkspaceInfo({ id });
@@ -32,6 +41,7 @@ function WorkspacePage() {
3241
moms={workspace.moms}
3342
/>
3443
)}
44+
<ConfMediaBar streams={confMediaStreams} />
3545
</div>
3646
);
3747
}

client/src/utils/rtc/index.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Socket } from 'socket.io-client';
2+
3+
type onMediaConnectedCb = (socketId: string, remoteStream: MediaStream) => void;
4+
type onMediaDisconnectedCb = (socketId: string) => void;
5+
6+
class RTC {
7+
static BITRATE = 30000;
8+
private signalingServerSocket: Socket;
9+
private iceServerUrls: string[];
10+
private userMediaStream: MediaStream;
11+
private connections: Map<string, RTCPeerConnection>;
12+
private streams: Map<string, MediaStream>;
13+
private onMediaConnectedCallback: onMediaConnectedCb;
14+
private onMediaDisconnectedCallback: onMediaDisconnectedCb;
15+
16+
constructor(signalingServerSocket: Socket, iceServerUrls: string[], userMediaStream: MediaStream) {
17+
this.signalingServerSocket = signalingServerSocket;
18+
this.iceServerUrls = iceServerUrls;
19+
this.userMediaStream = userMediaStream;
20+
this.connections = new Map();
21+
this.streams = new Map();
22+
this.onMediaConnectedCallback = () => { }; // eslint-disable-line @typescript-eslint/no-empty-function
23+
this.onMediaDisconnectedCallback = () => { }; // eslint-disable-line @typescript-eslint/no-empty-function
24+
}
25+
26+
onMediaConnected(callback: onMediaConnectedCb) {
27+
this.onMediaConnectedCallback = callback;
28+
}
29+
30+
onMediaDisconnected(callback: onMediaDisconnectedCb) {
31+
this.onMediaDisconnectedCallback = callback;
32+
}
33+
34+
#createPeerConnection(remoteSocketId: string) {
35+
// initialize
36+
const pcOptions = {
37+
iceServers: [ { urls: this.iceServerUrls }],
38+
};
39+
const pc = new RTCPeerConnection(pcOptions);
40+
41+
// add event listeners
42+
pc.addEventListener('icecandidate', iceEvent => {
43+
this.signalingServerSocket.emit('send_ice', iceEvent.candidate, remoteSocketId);
44+
});
45+
pc.addEventListener('track', async (event) => {
46+
if (this.streams.has(remoteSocketId)) {
47+
return;
48+
}
49+
50+
const [remoteStream] = event.streams;
51+
52+
this.streams.set(remoteSocketId, remoteStream);
53+
this.onMediaConnectedCallback(remoteSocketId, remoteStream);
54+
});
55+
56+
// add tracks
57+
this.userMediaStream.getTracks().forEach(track => pc.addTrack(track, this.userMediaStream));
58+
59+
return pc;
60+
}
61+
62+
async #setVideoBitrate(pc: RTCPeerConnection, bitrate: number) {
63+
// fetch video sender
64+
const [videoSender] = pc.getSenders().filter(sender => sender!.track!.kind === 'video');
65+
66+
// set bitrate
67+
const params = videoSender.getParameters();
68+
params.encodings[0].maxBitrate = bitrate;
69+
await videoSender.setParameters(params);
70+
}
71+
72+
connect() {
73+
this.signalingServerSocket.on('receive_hello', async (remoteSocketId) => {
74+
const pc = this.#createPeerConnection(remoteSocketId);
75+
this.connections.set(remoteSocketId, pc);
76+
77+
const offer = await pc.createOffer();
78+
await pc.setLocalDescription(offer);
79+
80+
this.signalingServerSocket.emit('send_offer', pc.localDescription, remoteSocketId);
81+
});
82+
83+
this.signalingServerSocket.on('receive_offer', async (offer, remoteSocketId) => {
84+
const pc = this.#createPeerConnection(remoteSocketId);
85+
this.connections.set(remoteSocketId, pc);
86+
87+
await pc.setRemoteDescription(offer);
88+
89+
const answer = await pc.createAnswer();
90+
await pc.setLocalDescription(answer);
91+
92+
this.signalingServerSocket.emit('send_answer', answer, remoteSocketId);
93+
});
94+
95+
this.signalingServerSocket.on('receive_answer', async (answer, remoteSocketId) => {
96+
const pc = this.connections.get(remoteSocketId);
97+
if (!pc) {
98+
throw new Error('No RTCPeerConnection on answer received.');
99+
}
100+
101+
await pc.setRemoteDescription(answer);
102+
});
103+
104+
this.signalingServerSocket.on('receive_ice', (ice, remoteSocketId) => {
105+
const pc = this.connections.get(remoteSocketId);
106+
console.log('received ice');
107+
if (!pc) {
108+
throw new Error('No RTCPeerConnection on ice candindate received.');
109+
}
110+
111+
pc.addIceCandidate(ice);
112+
});
113+
114+
this.signalingServerSocket.on('receive_bye', remoteSocketId => {
115+
this.connections.delete(remoteSocketId);
116+
this.streams.delete(remoteSocketId);
117+
118+
this.onMediaDisconnectedCallback(remoteSocketId);
119+
})
120+
121+
this.signalingServerSocket.emit('send_hello');
122+
}
123+
}
124+
125+
export default RTC;

0 commit comments

Comments
 (0)