Initial Commit

This commit is contained in:
2025-09-04 23:09:08 -04:00
commit 9149d0ac20
9 changed files with 2245 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.DS_Store
node_modules

1800
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "presentationpresenter",
"version": "1.0.0",
"description": "",
"module": "./src/index.ts",
"scripts": {
"start": "ts-node src/index.ts",
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "5.0.3",
"nodemon": "3.1.10",
"ts-node": "10.9.2",
"typescript": "5.9.2"
},
"dependencies": {
"express": "5.1.0",
"socket.io": "4.8.1"
}
}

47
public/index.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Dogeller</title>
<link
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="./styles.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
</head>
<body>
<div class="container">
<header class="header">
<div class="logo-container">
<img src="./img/doge.png" alt="doge logo" class="logo-img" />
<h1 class="logo-text">
Doge<span class="logo-highlight">ller</span>
</h1>
</div>
</header>
<div class="content-container">
<div class="active-users-panel" id="active-user-container">
<h3 class="panel-title">Active Users:</h3>
</div>
<div class="video-chat-container">
<h2 class="talk-info" id="talking-with-info">
Select active user on the left menu.
</h2>
<div class="video-container">
<video autoplay class="remote-video" id="remote-video"></video>
<video autoplay muted class="local-video" id="local-video"></video>
</div>
</div>
</div>
</div>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js" integrity="sha384-mkQ3/7FUtcGyoppY6bz/PORYoGqOl7/aSUMn2ymDOJcapfS6PHqxhRTMh1RR0Q6+" crossorigin="anonymous"></script>
<script src="./scripts/index.js"></script>
</body>
</html>

149
public/scripts/index.js Normal file
View File

@@ -0,0 +1,149 @@
let isAlreadyCalling = false;
let getCalled = false;
const existingCalls = [];
const { RTCPeerConnection, RTCSessionDescription } = window;
const peerConnection = new RTCPeerConnection();
function unselectUsersFromList() {
const alreadySelectedUser = document.querySelectorAll(
".active-user.active-user--selected"
);
alreadySelectedUser.forEach(el => {
el.setAttribute("class", "active-user");
});
}
function createUserItemContainer(socketId) {
const userContainerEl = document.createElement("div");
const usernameEl = document.createElement("p");
userContainerEl.setAttribute("class", "active-user");
userContainerEl.setAttribute("id", socketId);
usernameEl.setAttribute("class", "username");
usernameEl.innerHTML = `Socket: ${socketId}`;
userContainerEl.appendChild(usernameEl);
userContainerEl.addEventListener("click", () => {
unselectUsersFromList();
userContainerEl.setAttribute("class", "active-user active-user--selected");
const talkingWithInfo = document.getElementById("talking-with-info");
talkingWithInfo.innerHTML = `Talking with: "Socket: ${socketId}"`;
console.log('calling socket id [%o]', socketId);
callUser(socketId);
});
return userContainerEl;
}
async function callUser(socketId) {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(new RTCSessionDescription(offer));
socket.emit("call-user", {
offer,
to: socketId
});
}
function updateUserList(socketIds) {
const activeUserContainer = document.getElementById("active-user-container");
socketIds.forEach(socketId => {
if (socketId === socket.id) {
return;
}
const alreadyExistingUser = document.getElementById(socketId);
if (!alreadyExistingUser) {
const userContainerEl = createUserItemContainer(socketId);
activeUserContainer.appendChild(userContainerEl);
}
});
}
const socket = io.connect("localhost:5555");
socket.on("update-user-list", ({ users }) => {
updateUserList(users);
});
socket.on("remove-user", ({ socketId }) => {
const elToRemove = document.getElementById(socketId);
if (elToRemove) {
elToRemove.remove();
}
});
socket.on("call-made", async data => {
if (getCalled) {
const confirmed = confirm(
`User "Socket: ${data.socket}" wants to call you. Do accept this call?`
);
if (!confirmed) {
socket.emit("reject-call", {
from: data.socket
});
return;
}
}
await peerConnection.setRemoteDescription(
new RTCSessionDescription(data.offer)
);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(new RTCSessionDescription(answer));
socket.emit("make-answer", {
answer,
to: data.socket
});
getCalled = true;
});
socket.on("answer-made", async data => {
await peerConnection.setRemoteDescription(
new RTCSessionDescription(data.answer)
);
if (!isAlreadyCalling) {
callUser(data.socket);
isAlreadyCalling = true;
}
});
socket.on("call-rejected", data => {
alert(`User: "Socket: ${data.socket}" rejected your call.`);
unselectUsersFromList();
});
peerConnection.ontrack = function({ streams: [stream] }) {
const remoteVideo = document.getElementById("remote-video");
if (remoteVideo) {
remoteVideo.srcObject = stream;
}
};
navigator.getUserMedia(
{ video: true, audio: true },
stream => {
const localVideo = document.getElementById("local-video");
if (localVideo) {
localVideo.srcObject = stream;
}
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
},
error => {
console.warn(error.message);
}
);

105
public/styles.css Normal file
View File

@@ -0,0 +1,105 @@
body {
margin: 0;
padding: 0;
font-family: "Montserrat", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f9fafc;
color: #595354;
}
.header {
background-color: #ffffff;
padding: 10px 40px;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
}
.header > .logo-container {
display: flex;
align-items: center;
}
.header > .logo-container > .logo-img {
width: 60px;
height: 60px;
margin-right: 15px;
}
.header > .logo-container > .logo-text {
font-size: 26px;
font-weight: 700;
}
.header > .logo-container > .logo-text > .logo-highlight {
color: #65a9e5;
}
.content-container {
width: 100%;
height: calc(100vh - 89px);
display: flex;
justify-content: space-between;
overflow: hidden;
}
.active-users-panel {
width: 300px;
height: 100%;
border-right: 1px solid #cddfe7;
}
.panel-title {
margin: 10px 0 0 0;
padding-left: 30px;
font-weight: 500;
font-size: 18px;
border-bottom: 1px solid #cddfe7;
padding-bottom: 10px;
}
.active-user {
padding: 10px 30px;
border-bottom: 1px solid #cddfe7;
cursor: pointer;
user-select: none;
}
.active-user:hover {
background-color: #e8e9eb;
transition: background-color 0.5s ease;
}
.active-user--selected {
background-color: #fff;
border-right: 5px solid #65a9e5;
font-weight: 500;
transition: all 0.5s ease;
}
.video-chat-container {
padding: 0 20px;
flex: 1;
position: relative;
}
.talk-info {
font-weight: 500;
font-size: 21px;
}
.remote-video {
border: 1px solid #cddfe7;
width: 100%;
height: 100%;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.2);
}
.local-video {
position: absolute;
border: 1px solid #cddfe7;
bottom: 60px;
right: 40px;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.2);
border-radius: 5px;
width: 300px;
}

20
src/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import {Server} from './server';
const server = new Server();
process.on('SIGINT', () => {
console.log('Received [SIGINT]. Shutting down...');
server.dispose();
});
process.on('SIGTERM', () => {
console.log('Received [SIGTERM]. Shutting down...');
server.dispose();
});
process.on('SIGBREAK', () => {
console.log('Received [SIGBREAK]. Shutting down...');
server.dispose();
});
server.listen(port => {
console.log(`Server is listening on [http://localhost:${port}]`);
});

84
src/server.ts Normal file
View File

@@ -0,0 +1,84 @@
import path from 'path';
import express, { Application } from 'express';
import { Server as SocketIOServer, Socket } from 'socket.io';
import { createServer, Server as HTTPServer } from 'http';
export class Server {
private httpServer: HTTPServer;
private app: Application;
private io: SocketIOServer;
private activeSockets: Set<string> = new Set();
private readonly DEFAULT_PORT = 5555;
constructor() {
this.app = express();
this.httpServer = createServer(this.app);
this.io = new SocketIOServer(this.httpServer);
this.configureApp();
this.handleRoutes();
this.handleSocketConnection();
}
private configureApp(): void {
this.app.use(express.static(path.join(__dirname, "../public")));
}
private handleRoutes(): void {
this.app.get('/', (req, res) => {
res.send(`<h1>Hello World</h1>`);
});
}
private handleSocketConnection(): void {
this.io.on('connection', socket => {
if (!this.activeSockets.has(socket.id)) {
this.activeSockets.add(socket.id);
console.log(this.activeSockets.entries());
socket.emit("update-user-list", {
users: Array.from(this.activeSockets)
});
socket.broadcast.emit("update-user-list", {
users: [socket.id]
});
socket.on("disconnect", () => {
this.activeSockets.delete(socket.id);
socket.broadcast.emit("remove-user", {
socketId: socket.id
});
});
socket.on("call-user", data => {
console.log('[Server] call received [%o]', data);
socket.to(data.to).emit("call-made", {
offer: data.offer,
socket: socket.id
});
});
socket.on("make-answer", data => {
socket.to(data.to).emit("answer-made", {
socket: socket.id,
answer: data.answer
});
});
}
});
}
public listen(callback: (port: number) => void): void {
this.httpServer.listen(this.DEFAULT_PORT, () => {
callback(this.DEFAULT_PORT);
});
}
public dispose(): void {
this.httpServer.close();
this.io.close();
}
}

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}