Skip to content

Commit

Permalink
Merge pull request #157 from platinouss/feature/231127-multi-room
Browse files Browse the repository at this point in the history
Feature(#30, #45, #87, #146, #156): ๋ฏธ๋””์–ด ์„œ๋ฒ„์—์„œ ์—ฌ๋Ÿฌ ๊ฐ•์˜์‹ค์„ ๊ด€๋ฆฌํ•˜๊ณ , ํ™”์ดํŠธ๋ณด๋“œ & ์งˆ๋ฌธ & ๊ฐ•์˜ ์ข…๋ฃŒ & ๊ฐ•์˜ ๋‚˜๊ฐ€๊ธฐ๋ฅผ ์ง€์›ํ•œ๋‹ค
  • Loading branch information
tmddus2 authored Nov 28, 2023
2 parents ccbb6b4 + a3f6588 commit 2a5ba5f
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 90 deletions.
1 change: 1 addition & 0 deletions mediaServer/package-lock.json

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

2 changes: 2 additions & 0 deletions mediaServer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"build": "tsc",
"start": "tsc && node dist/main.js",
"dev": "nodemon --exec ts-node src/main.ts",
"windowDebug": "tsc && @powershell -Command $env:DEBUG='*';node dist/main.js",
"test": "jest"
},
"keywords": [],
Expand All @@ -17,6 +18,7 @@
"@types/node": "^20.9.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"debug": "^4.3.4",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
Expand Down
152 changes: 92 additions & 60 deletions mediaServer/src/RelayServer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Server, Socket } from 'socket.io';
import { RTCIceCandidate, RTCPeerConnection } from 'wrtc';
import dotenv from 'dotenv';
import { pc_config } from './pc.config';
import { RoomInfo } from './models/RoomInfo';
import { ClientInfo, ClientType } from './models/ClientInfo';
import { Message } from './models/Message';

export class RelayServer {
private readonly io;
private relayServerRTCPC: RTCPeerConnection;
private readonly studentRTCPCs: Map<string, RTCPeerConnection>;
private presenterStream: any;
private readonly roomsInfo: Map<string, RoomInfo>;
private readonly clientsInfo: Map<string, ClientInfo>;

constructor(port: number) {
this.relayServerRTCPC = new RTCPeerConnection();
this.studentRTCPCs = new Map();
this.roomsInfo = new Map();
this.clientsInfo = new Map();
this.io = new Server(port, {
cors: {
origin: '*',
Expand All @@ -26,36 +28,32 @@ export class RelayServer {
createRoom = (socket: Socket) => {
try {
socket.on('presenterOffer', async (data) => {
dotenv.config();
const pc_config = {
iceServers: [
{
urls: ['stun:stun.l.google.com:19302']
},
{
urls: process.env.TURN_URL as string,
username: process.env.TURN_USERNAME as string,
credential: process.env.TURN_PASSWORD as string
}
]
};
const RTCPC = new RTCPeerConnection(pc_config);
this.relayServerRTCPC = RTCPC;

this.clientsInfo.set(socket.id, new ClientInfo(ClientType.PRESENTER, RTCPC));
this.roomsInfo.set(data.roomId, new RoomInfo(data.roomId, socket, RTCPC));
RTCPC.ontrack = (event) => {
const stream = event.streams[0];
this.presenterStream = stream;
const roomInfo = this.roomsInfo.get(data.roomId);
if (roomInfo) {
roomInfo.stream = event.streams[0];
}
};

this.getServerCandidate(socket, data.socketId);
socket.join(socket.id);
this.exchangeCandidate('/create-room', socket);

await RTCPC.setRemoteDescription(data.SDP);
const SDP = await RTCPC.createAnswer();
socket.emit(`${data.socketId}-serverAnswer`, {
isStudent: false,
this.io.of('/create-room').to(socket.id).emit('serverAnswer', {
SDP: SDP
});
RTCPC.setLocalDescription(SDP);

const clientInfo = this.clientsInfo.get(socket.id);
if (!clientInfo) {
throw new Error('๋ฐœํ‘œ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
}
clientInfo.roomId = data.roomId;
socket.join(data.roomId);
});
} catch (e) {
console.log(e);
Expand All @@ -65,61 +63,95 @@ export class RelayServer {
enterRoom = (socket: Socket) => {
try {
socket.on('studentOffer', async (data) => {
const socketId = data.socketId;
const RTCPC = new RTCPeerConnection();

this.presenterStream.getTracks().forEach((track: any) => {
const RTCPC = new RTCPeerConnection(pc_config);
this.clientsInfo.set(socket.id, new ClientInfo(ClientType.STUDENT, RTCPC));
const presenterStream = this.roomsInfo.get(data.roomId)?.stream;
if (!presenterStream) {
return;
}
presenterStream.getTracks().forEach((track: any) => {
RTCPC.addTrack(track);
});

this.studentRTCPCs.set(socketId, RTCPC);

this.exchangeCandidate(socket, socketId);
socket.join(socket.id);
this.exchangeCandidate('/enter-room', socket);

await RTCPC.setRemoteDescription(data.SDP);
const SDP = await RTCPC.createAnswer();
socket.emit(`${data.socketId}-serverAnswer`, {
isStudent: true,
this.io.of('/enter-room').to(socket.id).emit(`serverAnswer`, {
SDP: SDP
});
RTCPC.setLocalDescription(SDP);

const roomInfo = this.roomsInfo.get(data.roomId);
const clientInfo = this.clientsInfo.get(socket.id);
if (!clientInfo || !roomInfo) {
throw new Error('๋ฐœํ‘œ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
}
roomInfo.studentSocketList.add(socket);
clientInfo.roomId = data.roomId;
socket.join(data.roomId);
});
} catch (e) {
console.log(e);
}
};

getServerCandidate = (socket: Socket, presenterSocketId: string) => {
lecture = (socket: Socket) => {
const clientInfo = this.clientsInfo.get(socket.id);
if (!clientInfo || !clientInfo.roomId) {
throw new Error('์ž˜๋ชป๋œ ์‚ฌ์šฉ์ž ์ž…๋‹ˆ๋‹ค.');
}
socket.on('editBoard', (data) => {
if (clientInfo.type !== ClientType.PRESENTER) {
throw new Error('ํ•ด๋‹น ๋ฐœํ‘œ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
}
this.io.of('/lecture').to(clientInfo.roomId).emit('edit', new Message('whiteBoard', data.content));
});
socket.on('question', (data) => {
if (clientInfo.type !== ClientType.STUDENT) {
throw new Error('ํ•ด๋‹น ์ฐธ์—ฌ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
}
const presenterSocket = this.roomsInfo.get(clientInfo.roomId)?.presenterSocket;
if (!presenterSocket) {
throw new Error('ํ•ด๋‹น ๋ฐฉ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค');
}
this.io.of('/lecture').to(presenterSocket.id).emit('question', new Message('question', data.content));
});
socket.on('endOfLecture', (data) => {
if (clientInfo.type !== ClientType.PRESENTER) {
throw new Error('ํ•ด๋‹น ๋ฐœํ‘œ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค');
}
this.io.of('/lecture').to(clientInfo.roomId).emit('end', new Message('lecture', data.content));
this.roomsInfo.get(clientInfo.roomId)?.endLecture();
this.roomsInfo.delete(clientInfo.roomId);
clientInfo.roomId = '';
});
socket.on('leaveLecture', () => {
if (clientInfo.type !== ClientType.STUDENT) {
throw new Error('ํ•ด๋‹น ์ฐธ์—ฌ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค');
}
this.roomsInfo.get(clientInfo.roomId)?.exitRoom(socket);
this.io.of('/lecture').to(clientInfo.roomId).emit('exit', new Message('lecture', 'success'));
});
};

exchangeCandidate = (namespace: string, socket: Socket) => {
try {
this.relayServerRTCPC.onicecandidate = (e) => {
const RTCPC = this.clientsInfo.get(socket.id)?.RTCPC;
if (!RTCPC) {
console.log('Unable to exchange candidates');
return;
}
RTCPC.onicecandidate = (e) => {
if (e.candidate) {
socket.emit(`${presenterSocketId}-serverCandidate`, {
this.io.of(namespace).to(socket.id).emit(`serverCandidate`, {
candidate: e.candidate
});
}
};
socket.on('presenterCandidate', (data) => {
this.relayServerRTCPC.addIceCandidate(new RTCIceCandidate(data.candidate));
});
} catch (e) {
console.log(e);
}
};

exchangeCandidate = (socket: Socket, socketId: any) => {
try {
const RTCPC = this.studentRTCPCs.get(socketId);
if (RTCPC) {
RTCPC.onicecandidate = (e) => {
if (e.candidate) {
socket.emit(`${socketId}-serverCandidate`, {
candidate: e.candidate
});
}
};
}
socket.on('studentCandidate', (data) => {
RTCPC?.addIceCandidate(new RTCIceCandidate(data.candidate));
socket.on('clientCandidate', (data) => {
RTCPC.addIceCandidate(new RTCIceCandidate(data.candidate));
});
} catch (e) {
console.log(e);
Expand Down
1 change: 1 addition & 0 deletions mediaServer/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const PORT = 3000;
const relayServer = new RelayServer(PORT);
relayServer.listen('/create-room', 'connection', relayServer.createRoom);
relayServer.listen('/enter-room', 'connection', relayServer.enterRoom);
relayServer.listen('/lecture', 'connection', relayServer.lecture);

const app = http.createServer((req, res) => {
try {
Expand Down
30 changes: 30 additions & 0 deletions mediaServer/src/models/ClientInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { RTCPeerConnection } from 'wrtc';

export enum ClientType {
PRESENTER,
STUDENT
}

export class ClientInfo {
private readonly _type: ClientType;
private readonly _RTCPC: RTCPeerConnection;
private _roomId: string;

constructor(type: ClientType, RTCPC: RTCPeerConnection) {
this._type = type;
this._RTCPC = RTCPC;
this._roomId = '';
}

get type(): ClientType {
return this._type;
}

get RTCPC(): RTCPeerConnection {
return this._RTCPC;
}

set roomId(roomId: string) {
this._roomId = roomId;
}
}
9 changes: 9 additions & 0 deletions mediaServer/src/models/Message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class Message {
type: string;
content: string;

constructor(type: string, content: string) {
this.type = type;
this.content = content;
}
}
46 changes: 46 additions & 0 deletions mediaServer/src/models/RoomInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { RTCPeerConnection } from 'wrtc';
import { Socket } from 'socket.io';

export class RoomInfo {
private readonly _roomId: string;
private readonly _RTCPC: RTCPeerConnection;
private readonly _presenterSocket: Socket;
private readonly _studentSocketList: Set<Socket>;
private _stream: MediaStream | null;

constructor(roomId: string, socket: Socket, RTCPC: RTCPeerConnection) {
this._roomId = roomId;
this._presenterSocket = socket;
this._studentSocketList = new Set();
this._RTCPC = RTCPC;
this._stream = null;
}

get presenterSocket(): Socket {
return this._presenterSocket;
}

get studentSocketList(): Set<Socket> {
return this._studentSocketList;
}

set stream(presenterStream: MediaStream) {
this._stream = presenterStream;
}

get stream(): MediaStream | null {
return this._stream;
}

endLecture = () => {
this._studentSocketList.forEach((student: Socket) => {
student.leave(this._roomId);
});
this._presenterSocket.leave(this._roomId);
};

exitRoom = (studentSocket: Socket) => {
studentSocket.leave(this._roomId);
this._studentSocketList.delete(studentSocket);
};
}
16 changes: 16 additions & 0 deletions mediaServer/src/pc.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import dotenv from 'dotenv';

dotenv.config();

export const pc_config = {
iceServers: [
{
urls: ['stun:stun.l.google.com:19302']
},
{
urls: process.env.TURN_URL as string,
username: process.env.TURN_USERNAME as string,
credential: process.env.TURN_PASSWORD as string
}
]
};
Loading

0 comments on commit 2a5ba5f

Please sign in to comment.