Skip to content

Commit

Permalink
merge: [FE] 프론트엔드 WebRTC 구현
Browse files Browse the repository at this point in the history
merge: [FE] 프론트엔드 WebRTC 구현
  • Loading branch information
d0422 authored Nov 10, 2023
2 parents b5f9088 + 30298f1 commit 49fa05d
Show file tree
Hide file tree
Showing 11 changed files with 299 additions and 12 deletions.
2 changes: 2 additions & 0 deletions frontEnd/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ module.exports = {
plugins: ['react', '@typescript-eslint', 'prettier'],
rules: {
'react/react-in-jsx-scope': 'off',
'import/extensions': ['off'],
'react-hooks/exhaustive-deps': 'off',
},
};
97 changes: 93 additions & 4 deletions frontEnd/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion frontEnd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.18.0"
"react-router-dom": "^6.18.0",
"socket.io-client": "^4.7.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.4",
Expand All @@ -22,6 +23,7 @@
"@types/node": "^20.8.10",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.7",
"@types/socket.io-client": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.0.3",
Expand Down
13 changes: 13 additions & 0 deletions frontEnd/src/constants/socketEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const SOCKET_RECEIVE_EVENT = {
ALL_USERS: 'all_users',
OFFER: 'getOffer',
ANSWER: 'getAnswer',
CANDIDATE: 'getCandidate',
USER_EXIT: 'user_exit',
};

export const SOCKET_EMIT_EVENT = {
OFFER: 'offer',
ANSWER: 'answer',
JOIN_ROOM: 'join_room',
};
2 changes: 2 additions & 0 deletions frontEnd/src/constants/urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const SOCKET_URL = `ws://localhost:4000`;
export const API_URL = `http://localhost:4000`;
7 changes: 1 addition & 6 deletions frontEnd/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Home from '@pages/Home.tsx';
Expand All @@ -15,8 +14,4 @@ const router = createBrowserRouter([
element: <Room />,
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);
ReactDOM.createRoot(document.getElementById('root')!).render(<RouterProvider router={router} />);
47 changes: 46 additions & 1 deletion frontEnd/src/pages/Room.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
import { useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { createSocket } from '@/services/Socket';
import { StreamObject, streamModel } from '@/stores/StreamModel';
import { SOCKET_EMIT_EVENT } from '@/constants/socketEvents';

function StreamVideo({ stream }: { stream: MediaStream }) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (!videoRef.current) return;
videoRef.current.srcObject = stream;
}, []);

return <video key={stream.id} ref={videoRef} muted autoPlay />;
}

export default function Room() {
return <div>Room</div>;
const videoRef = useRef<HTMLVideoElement>(null);
const [streamList, setStreamList] = useState<StreamObject[]>([]);
const { roomId } = useParams();

useEffect(() => {
navigator.mediaDevices
.getUserMedia({
video: true,
})
.then((video) => {
(videoRef.current as HTMLVideoElement).srcObject = video;
streamModel.subscribe(() => {
setStreamList(() => streamModel.getStream());
});
const socket = createSocket(video);
socket.connect();
socket.emit(SOCKET_EMIT_EVENT.JOIN_ROOM, {
room: roomId,
});
});
}, []);

return (
<div>
<video ref={videoRef} muted autoPlay />
{streamList.map(({ id, stream }) => {
return <StreamVideo key={id} stream={stream} />;
})}
</div>
);
}
29 changes: 29 additions & 0 deletions frontEnd/src/services/RTC.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Socket } from 'socket.io-client/debug';
import { streamModel } from '@/stores/StreamModel';

const createPeerConnection = (socketId: string, socket: Socket, localStream: MediaStream) => {
const RTCConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.1.google.com:19302' }],
});
if (localStream) {
localStream.getTracks().forEach((track) => {
RTCConnection.addTrack(track, localStream);
});
}
RTCConnection.addEventListener('icecandidate', (e) => {
if (e.candidate != null)
socket.emit('candidate', {
candidate: e.candidate,
candidateSendId: socket.id,
candidateReceiveId: socketId,
});
});

RTCConnection.addEventListener('track', (e) => {
streamModel.addStream({ id: socketId, stream: e.streams[0] });
});

return RTCConnection;
};

export default createPeerConnection;
64 changes: 64 additions & 0 deletions frontEnd/src/services/Socket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { io } from 'socket.io-client/debug';
import { SOCKET_URL } from '@/constants/urls';
import createPeerConnection from './RTC';
import { SOCKET_EMIT_EVENT, SOCKET_RECEIVE_EVENT } from '@/constants/socketEvents';
import { streamModel } from '@/stores/StreamModel';

export const RTCConnectionList: Record<string, RTCPeerConnection> = {};

export const createSocket = (localStream: MediaStream) => {
const socket = io(SOCKET_URL);

socket.on(SOCKET_RECEIVE_EVENT.ALL_USERS, async (data: { users: Array<{ id: string }> }) => {
data.users.forEach((user) => {
RTCConnectionList[user.id] = createPeerConnection(user.id, socket, localStream);
});

Object.entries(RTCConnectionList).forEach(async ([key, value]) => {
const offer = await value.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});

await value.setLocalDescription(new RTCSessionDescription(offer));
socket.emit(SOCKET_EMIT_EVENT.OFFER, {
sdp: offer,
offerSendId: socket.id,
offerReceiveId: key,
});
});
});

socket.on(SOCKET_RECEIVE_EVENT.OFFER, async (data: { sdp: RTCSessionDescription; offerSendId: string }) => {
RTCConnectionList[data.offerSendId] = createPeerConnection(data.offerSendId, socket, localStream);
await RTCConnectionList[data.offerSendId].setRemoteDescription(new RTCSessionDescription(data.sdp));
const answer = await RTCConnectionList[data.offerSendId].createAnswer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});

await RTCConnectionList[data.offerSendId].setLocalDescription(new RTCSessionDescription(answer));
socket.emit(SOCKET_EMIT_EVENT.ANSWER, {
sdp: answer,
answerSendId: socket.id,
answerReceiveId: data.offerSendId,
});
});

socket.on(SOCKET_RECEIVE_EVENT.ANSWER, (data: { sdp: RTCSessionDescription; answerSendId: string }) => {
RTCConnectionList[data.answerSendId].setRemoteDescription(new RTCSessionDescription(data.sdp));
});

socket.on(SOCKET_RECEIVE_EVENT.CANDIDATE, (data: { candidate: RTCIceCandidateInit; candidateSendId: string }) => {
if (RTCConnectionList[data.candidateSendId].remoteDescription)
RTCConnectionList[data.candidateSendId].addIceCandidate(new RTCIceCandidate(data.candidate));
});

socket.on(SOCKET_RECEIVE_EVENT.USER_EXIT, (data: { id: string }) => {
RTCConnectionList[data.id].close();
delete RTCConnectionList[data.id];
streamModel.removeStream(data.id);
});

return socket;
};
31 changes: 31 additions & 0 deletions frontEnd/src/stores/StreamModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Observer from '@/utils/Observer';

export interface StreamObject {
id: string;
stream: MediaStream;
}

export class StreamModel extends Observer {
streams: StreamObject[];

constructor() {
super();
this.streams = [];
}

getStream() {
return [...this.streams];
}

addStream(stream: StreamObject) {
this.streams.push(stream);
this.notify();
}

removeStream(id: string) {
this.streams = this.streams.filter((stream) => stream.id !== id);
this.notify();
}
}

export const streamModel = new StreamModel();
Loading

0 comments on commit 49fa05d

Please sign in to comment.