([]);
+ 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 (
+
+
+ {streamList.map(({ id, stream }) => {
+ return ;
+ })}
+
+ );
}
diff --git a/frontEnd/src/services/RTC.ts b/frontEnd/src/services/RTC.ts
new file mode 100644
index 0000000..bbef66b
--- /dev/null
+++ b/frontEnd/src/services/RTC.ts
@@ -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;
diff --git a/frontEnd/src/services/Socket.ts b/frontEnd/src/services/Socket.ts
new file mode 100644
index 0000000..ee48ab3
--- /dev/null
+++ b/frontEnd/src/services/Socket.ts
@@ -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 = {};
+
+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;
+};
diff --git a/frontEnd/src/stores/StreamModel.ts b/frontEnd/src/stores/StreamModel.ts
new file mode 100644
index 0000000..98f4353
--- /dev/null
+++ b/frontEnd/src/stores/StreamModel.ts
@@ -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();
diff --git a/frontEnd/src/utils/Observer.ts b/frontEnd/src/utils/Observer.ts
new file mode 100644
index 0000000..772b629
--- /dev/null
+++ b/frontEnd/src/utils/Observer.ts
@@ -0,0 +1,15 @@
+export default class Observer {
+ observers: Set<() => void>;
+
+ constructor() {
+ this.observers = new Set();
+ }
+
+ subscribe(func: () => void) {
+ this.observers.add(func);
+ }
+
+ notify() {
+ this.observers.forEach((func) => func());
+ }
+}